///|
/// Contexts, entries and the three operators of Figure 2.2 and Definition 3.1.
///
/// A port of the reference's `contexts.py`. The context is an IMMUTABLE sorted
/// map: immutable because contexts are threaded functionally through every
/// rule and nothing may see a later branch's bindings, sorted because two
/// messages -- the submodule clash and the mutual-region duplicate -- name
/// `sorted(...)[0]`, and an accidental order would name a different variable.
pub type Context = @sorted_map.SortedMap[String, Entry]

///|
/// Whether a variable is definitely assigned. The spec writes `tt` and `ff`.
pub(all) enum Status {
  TT
  FF
} derive(Eq, Debug, Compare)

///|
/// What a name can stand for.
pub(all) enum Entry {
  /// A variable, definitely assigned or not.
  Var(Status)
  /// A module named but not loaded: `import a.b` puts `a.b` in scope as a
  /// stub until something loads it.
  ModuleStub(String)
  /// A module whose members are known.
  ModuleLoaded(String, Context)
  /// A class declaration.
  Class(ClassEntry)
} derive(Eq, Debug)

///|
/// A class, with the context it was DECLARED in.
///
/// The declaring context is the load-bearing part: `fields` and `ancestors`
/// resolve the base class through it and not through whatever context is
/// current, so a class that is imported keeps the meaning it had where it was
/// written.
pub(all) struct ClassEntry {
  context : Context
  name : String
  own_fields : Array[String]
  base : String?
} derive(Eq, Debug)

///|
/// A module being checked: the context, the program it belongs to, and its own
/// qualified name.
pub(all) struct ModuleContext[M] {
  gamma : Context
  program : M
  q : String
} derive(Eq, Debug)

///|
pub fn empty_context() -> Context {
  @sorted_map.SortedMap::new()
}

///|
pub fn context_of(entries : Array[(String, Entry)]) -> Context {
  @sorted_map.SortedMap(entries[:])
}

// ---------------------------------------------------------------------------
// Lookup

///|
pub fn[M] ModuleContext::override_gamma(
  self : ModuleContext[M],
  delta : Context,
) -> ModuleContext[M] {
  let mut g = self.gamma
  delta.each(fn(k, v) { g = g.add(k, v) })
  { ..self, gamma: g, }
}

///|
pub fn[M] ModuleContext::override_var(
  self : ModuleContext[M],
  delta : VarContext,
) -> ModuleContext[M] {
  let mut g = self.gamma
  delta.each(fn(k, v) { g = g.add(k, Var(v)) })
  { ..self, gamma: g, }
}

///|
/// A delta of variables only: what a statement's result type carries.
pub type VarContext = @sorted_map.SortedMap[String, Status]

///|
pub fn empty_vars() -> VarContext {
  @sorted_map.SortedMap::new()
}

///|
pub fn vars_of(entries : Array[(String, Status)]) -> VarContext {
  @sorted_map.SortedMap(entries[:])
}

///|
pub fn[M] ModuleContext::var_status(
  self : ModuleContext[M],
  x : String,
) -> Status? {
  match self.gamma.get(x) {
    Some(Var(s)) => Some(s)
    _ => None
  }
}

///|
pub fn[M] ModuleContext::class_of(
  self : ModuleContext[M],
  c : String,
) -> ClassEntry? {
  match self.gamma.get(c) {
    Some(Class(e)) => Some(e)
    _ => None
  }
}

///|
/// The module a name stands for, loaded or not.
pub fn[M] ModuleContext::module_of(
  self : ModuleContext[M],
  x : String,
) -> Entry? {
  match self.gamma.get(x) {
    Some(ModuleStub(_) as e) | Some(ModuleLoaded(_, _) as e) => Some(e)
    _ => None
  }
}

///|
/// What a qualified name stands for: a name in the context, or a member of a
/// loaded module reached through it.
pub fn[M] entry_of(e : @ast.Expr, ctx : ModuleContext[M]) -> Entry? {
  match e {
    Name(id~, ..) => ctx.gamma.get(id)
    Attribute(value~, attr~, ..) =>
      match entry_of(value, ctx) {
        Some(ModuleLoaded(_, members)) => members.get(attr)
        _ => None
      }
    _ => None
  }
}

///|
pub fn[M] class_entry(e : @ast.Expr, ctx : ModuleContext[M]) -> ClassEntry? {
  match entry_of(e, ctx) {
    Some(Class(c)) => Some(c)
    _ => None
  }
}

// ---------------------------------------------------------------------------
// Classes

///|
/// The last segment of a class's qualified name -- what a message prints.
pub fn ClassEntry::short_name(self : ClassEntry) -> String {
  match self.name.rev_split_once(".") {
    Some((_, last)) => last.to_owned()
    None => self.name
  }
}

///|
/// The class and every class it inherits from, nearest first.
pub fn ClassEntry::ancestors(self : ClassEntry) -> Array[ClassEntry] {
  match self.base {
    None => [self]
    Some(b) =>
      match self.context.get(b) {
        Some(Class(base)) => [self] + base.ancestors()
        _ => abort("a class's base must be a class in its declaring context")
      }
  }
}

///|
/// Every field, INHERITED FIRST -- which is the order a constructor's
/// positional arguments are in and the order an object prints in.
pub fn ClassEntry::fields(self : ClassEntry) -> Array[String] {
  match self.base {
    None => self.own_fields
    Some(b) =>
      match self.context.get(b) {
        Some(Class(base)) => base.fields() + self.own_fields
        _ => abort("a class's base must be a class in its declaring context")
      }
  }
}

///|
/// Match a constructor's or a pattern's arguments to the class's fields.
///
/// `None` when the call is over- or under-saturated, when a keyword is
/// repeated, or when the keywords are not exactly the fields the positional
/// arguments did not cover.
pub fn[T] ClassEntry::field_map(
  self : ClassEntry,
  positional : Array[T],
  kwd_names : Array[String],
  kwd_values : Array[T],
) -> Array[(String, T)]? {
  let xs = self.fields()
  let n = positional.length()
  if n + kwd_names.length() != xs.length() {
    return None
  }
  let seen = @sorted_map.SortedMap::from_iter(
    kwd_names.iter().map(fn(k) { (k, true) }),
  )
  if seen.length() != kwd_names.length() {
    return None
  }
  let rest = @sorted_map.SortedMap::from_iter(
    xs[n:].iter().map(fn(k) { (k, true) }),
  )
  if seen.length() != rest.length() {
    return None
  }
  for k in kwd_names {
    if !rest.contains(k) {
      return None
    }
  }
  let out : Array[(String, T)] = []
  for i in 0.. ResultType {
  Assigns(empty_vars())
}

///|
/// `tt` only when both sides say so.
pub fn merge_status(a : Status, b : Status) -> Status {
  if a is TT && b is TT {
    TT
  } else {
    FF
  }
}

///|
/// The merge of two branches' deltas: a name assigned on one side only is
/// present but not definitely assigned.
pub fn merge_delta(d1 : VarContext, d2 : VarContext) -> VarContext {
  let mut out = empty_vars()
  d1.each(fn(k, a) {
    out = out.add(
      k,
      match d2.get(k) {
        Some(b) => merge_status(a, b)
        None => FF
      },
    )
  })
  d2.each(fn(k, b) {
    if !d1.contains(k) {
      out = out.add(k, FF)
    } else {
      ignore(b)
    }
  })
  out
}

///|
/// The merge of a match's or an if's branches. A branch that returns
/// contributes nothing; if every branch returns, so does the whole.
pub fn merge_results(rs : Array[ResultType]) -> ResultType {
  let branches : Array[VarContext] = []
  for r in rs {
    match r {
      Assigns(d) => branches.push(d)
      Returns => ()
    }
  }
  if branches.is_empty() {
    return Returns
  }
  let mut acc = branches[0]
  for i in 1.. VarContext {
  let mut out = d1
  d2.each(fn(k, v) { out = out.add(k, v) })
  out
}

///|
/// Sequencing: once anything returns, the sequence returns.
pub fn override_results(r1 : ResultType, r2 : ResultType) -> ResultType {
  match (r1, r2) {
    (Returns, _) => Returns
    (_, Returns) => Returns
    (Assigns(a), Assigns(b)) => Assigns(override_delta(a, b))
  }
}

// ---------------------------------------------------------------------------
// Extension: what an import adds to a context

///|
/// Extending prefers a loaded module over a stub of the same module, and
/// merges two loadings of the same module member by member. Anything else is
/// simply replaced.
pub fn extend_entry(a : Entry, b : Entry) -> Entry {
  match (a, b) {
    (ModuleLoaded(qa, ma), ModuleLoaded(qb, mb)) if qa == qb =>
      ModuleLoaded(qa, extend_context(ma, mb))
    (ModuleLoaded(qa, _), ModuleStub(qb)) if qa == qb => a
    _ => b
  }
}

///|
pub fn extend_context(g1 : Context, g2 : Context) -> Context {
  let mut out = g1
  g2.each(fn(x, e) {
    out = out.add(
      x,
      match g1.get(x) {
        Some(old) => extend_entry(old, e)
        None => e
      },
    )
  })
  out
}

// ---------------------------------------------------------------------------
// The predefined modules

///|
/// Figure 2.7's modules, and what each exports.
///
/// `typing` exposes only `Any` here. Figure 2.7 of the spec also lists
/// `Callable`; the reference does not, and the reference is the oracle. The
/// divergence is recorded rather than reconciled, to be revisited when
/// upstream settles it.
pub fn predefined_members(
  q : String,
  profile? : @profile.Profile = @profile.core,
) -> Array[String]? {
  // The core names come first and in this order under every profile. Not for
  // tidiness: `check_submodule_clash` walks a module's own members and reports
  // the first clash it finds, so reordering them would reorder a diagnostic
  // the reference decides.
  if q == "builtins" && profile.has(ExtraBuiltins) {
    let out = ["print", "len", "range"]
    for n in extra_builtin_names {
      out.push(n)
    }
    return Some(out)
  }
  match q {
    "builtins" => Some(["print", "len", "range"])
    "math" =>
      Some([
        "pi", "e", "sqrt", "exp", "log", "sin", "cos", "tan", "floor", "ceil",
      ])
    "sys" => Some(["argv", "exit"])
    "typing" => Some(["Any"])
    "dataclasses" => Some(["dataclass"])
    _ => None
  }
}

///|
pub let predefined_modules : Array[String] = [
  "builtins", "dataclasses", "math", "sys", "typing",
]

///|
/// The names `Feature::ExtraBuiltins` adds to `builtins`, in a fixed order.
///
/// The evaluator has a value for every one of them, and
/// `lib/eval/predefined_wbtest.mbt` insists on it -- the two tables cannot BE
/// one, because `lib/check` does not import `lib/value` and a bridge between
/// them would put a runtime type on the checker's side of the layering.
///
/// Everything here is pure, and everything here is TOTAL or cleanly partial:
/// where Python would answer by coercing -- `sum([True])`, `any([1])`,
/// `int(True)` -- PurePy has no rule and the call is undefined rather than
/// guessing. `bool` is absent for that reason and not by oversight: it is
/// Python's truthiness in a function, and PurePy has no truthiness at all.
pub let extra_builtin_names : Array[String] = [
  "abs", "all", "any", "divmod", "enumerate", "float", "int", "list", "max", "min",
  "repr", "reversed", "round", "sorted", "str", "sum", "tuple", "zip",
]

///|
/// Every predefined module's members are definitely assigned, and so is
/// `__name__`, which every module has.
pub fn predefined_context(
  q : String,
  profile? : @profile.Profile = @profile.core,
) -> Context {
  let entries : Array[(String, Entry)] = [("__name__", Var(TT))]
  match predefined_members(q, profile~) {
    Some(ms) =>
      for m in ms {
        entries.push((m, Var(TT)))
      }
    None => ()
  }
  context_of(entries)
}

///|
/// The fields a class declares, from its declaration.
pub fn own_fields(node : @ast.Stmt) -> Array[String] {
  @analysis.own_fields(node)
}