///|
/// The runtime error raised during evaluation, wrapping a structured
/// `@errors.EvalError` (message + call-stack backtrace).
priv suberror EvalErr {
  EvalErr(@errors.EvalError)
}

///|
/// A two-level name environment: per-execution bindings (a custom universe,
/// per-call predeclared values, or an expression `env`) layered over a base of
/// the universal constants and the builtins. Used only to resolve predeclared
/// and universal names for the VM; module globals live in the VM's own slot
/// array, not here.
priv struct EvalEnv {
  locals : Map[String, @value.Value]
  parent : EvalEnv?
}

///|
priv struct EvalContext {
  thread : Thread
  opts : Options
  global_env : EvalEnv
  // The bytecode VM driving execution, so a builtin that calls back into a
  // compiled function (e.g. `sorted(iterable, key)`) re-enters the VM.
  mut vm : VM?
}

///|
fn EvalEnv::new() -> EvalEnv {
  { locals: Map([]), parent: None }
}

///|
fn EvalEnv::for_module(parent : EvalEnv) -> EvalEnv {
  { locals: Map([]), parent: Some(parent) }
}

///|
/// Resolves `name`, searching this environment then its parent; `None` if
/// unbound.
fn EvalEnv::lookup(self : EvalEnv, name : String) -> @value.Value? {
  match self.locals.get(name) {
    Some(v) => Some(v)
    None =>
      match self.parent {
        Some(p) => p.lookup(name)
        None => None
      }
  }
}

///|
fn EvalEnv::bind(self : EvalEnv, name : String, v : @value.Value) -> Unit {
  self.locals[name] = v
}

///|
fn EvalContext::new(thread : Thread, opts : Options) -> EvalContext {
  let base = EvalEnv::new()
  base.bind("None", @value.Value::None)
  base.bind("True", @value.Value::Bool(true))
  base.bind("False", @value.Value::Bool(false))
  for name in builtin_names {
    base.bind(
      name,
      @value.Value::Builtin(@value.StarlarkBuiltinFunc::dispatch(name)),
    )
  }
  let global_env = EvalEnv::for_module(base)
  { thread, opts, global_env, vm: None }
}

///|
fn make_eval_error(ctx : EvalContext, msg : String) -> @errors.EvalError {
  let frames : Array[@errors.CallFrame] = []
  for frame in ctx.thread.call_stack {
    frames.push(frame)
  }
  @errors.EvalError::with_stack(msg, @errors.CallStack::new(frames))
}

///|
fn check_steps(ctx : EvalContext) -> Unit raise EvalErr {
  match ctx.thread.cancel_reason {
    Some(reason) =>
      raise EvalErr(
        make_eval_error(ctx, "Starlark computation cancelled: \{reason}"),
      )
    None => ()
  }
  ctx.thread.steps += 1
  match ctx.thread.max_steps {
    None => ()
    Some(max) =>
      if ctx.thread.steps > max {
        match ctx.thread.on_max_steps {
          Some(cb) => {
            cb(ctx.thread)
            match ctx.thread.cancel_reason {
              Some(reason) =>
                raise EvalErr(
                  make_eval_error(
                    ctx,
                    "Starlark computation cancelled: \{reason}",
                  ),
                )
              None => ()
            }
          }
          None => {
            ctx.thread.cancel("too many steps")
            raise EvalErr(
              make_eval_error(
                ctx, "Starlark computation cancelled: too many steps",
              ),
            )
          }
        }
      }
  }
}

///|
fn[T] check(ctx : EvalContext, r : Result[T, String]) -> T raise EvalErr {
  match r {
    Ok(v) => v
    Err(msg) => raise EvalErr(make_eval_error(ctx, msg))
  }
}

///|
fn[T] check_pfx(
  ctx : EvalContext,
  prefix : String,
  r : Result[T, String],
) -> T raise EvalErr {
  match r {
    Ok(v) => v
    Err(msg) => raise EvalErr(make_eval_error(ctx, "\{prefix}: \{msg}"))
  }
}

///|
fn require_seq(
  ctx : EvalContext,
  name : String,
  v : @value.Value,
) -> @value.StarlarkIterator raise EvalErr {
  match @value.iterate(v) {
    Ok(it) => it
    Err(_) =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{name}: for parameter 1: got \{v.type_name()}, want iterable",
        ),
      )
  }
}

///|
/// Raises a graceful error when `v` is a `Range` whose length would exceed the
/// backend-specific allocation cap (`max_alloc_elems`). Must be called before
/// any `require_seq(…).collect()` or equivalent unbounded collect loop so that
/// oversized ranges fail gracefully instead of triggering an OS overcommit
/// page-fault on the native backend.
fn guard_range_alloc(
  ctx : EvalContext,
  caller : String,
  v : @value.Value,
) -> Unit raise EvalErr {
  if v is @value.Value::Range(r) {
    let len = r.length()
    if len < 0L {
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{caller}: excessive sequence (range length overflows)",
        ),
      )
    }
    if len >= max_alloc_elems.to_int64() {
      raise EvalErr(
        make_eval_error(ctx, "\{caller}: excessive sequence (\{len} elements)"),
      )
    }
  }
}

///|
/// Raises a graceful error when `current_len` — the current element count of
/// an accumulator container — has reached the backend-specific allocation cap
/// (`max_alloc_elems`). Called unconditionally for `Append` (every append
/// grows the list). For `SetAdd` / `SetDict` / `SetDictUniq`, call
/// `guard_accum_alloc_if_new` instead, which skips this check when the
/// container would not grow.
fn guard_accum_alloc(
  ctx : EvalContext,
  caller : String,
  current_len : Int,
) -> Unit raise EvalErr {
  if current_len >= max_alloc_elems {
    raise EvalErr(
      make_eval_error(
        ctx,
        "\{caller}: excessive sequence (\{current_len + 1} elements)",
      ),
    )
  }
}

///|
/// Returns `false` when `result` is `Err`, collapsing the error branch to the
/// "absent" signal. This is the canonical presence-check shape for accumulator
/// opcodes: an `Err` means the element is unhashable, and we intentionally
/// swallow it here so the actual insert (`s.add` / `d.set`) runs next and
/// surfaces the hashability error through the normal check path.
fn swallowed_contains(result : Result[Bool, String]) -> Bool {
  match result {
    Ok(b) => b
    Err(_) => false
  }
}

///|
/// Returns `true` only when `result` is `Ok(false)`, confirming the element
/// is absent from the set. `Ok(true)` and `Err` both collapse to `false` so
/// that only confirmed-absent elements enter a difference result. This is the
/// inverse Err policy from `swallowed_contains`: `Err` is treated as "present"
/// (skip), not "absent" (add).
fn confirmed_absent(result : Result[Bool, String]) -> Bool {
  match result {
    Ok(false) => true
    _ => false
  }
}

///|
/// Guards the allocation cap only when the container will actually grow (i.e.,
/// `is_present` is `false`). When `is_present` is `true` the container does
/// not grow, so the guard is skipped. For `SetAdd` and `SetDict` this means
/// the existing entry is silently updated. `SetDictUniq` never reaches this
/// call with `is_present == true`: it raises the duplicate-key error upstream
/// before calling this function.
///
/// Invariant: `is_present` must be the result of `swallowed_contains` applied
/// to the container's `contains` call, so that unhashable elements are treated
/// as absent and the alloc guard fires *before* the hashability error.
fn guard_accum_alloc_if_new(
  ctx : EvalContext,
  caller : String,
  current_len : Int,
  is_present : Bool,
) -> Unit raise EvalErr {
  if !is_present {
    guard_accum_alloc(ctx, caller, current_len)
  }
}