///|
/// A source location using 1-based line and column numbers, matching
/// starlark-go's `syntax.Position` convention. Line 0 means unknown.
pub struct Position {
  priv filename : String
  priv line : Int
  priv col : Int
}

///|
/// Creates a `Position`. Use line=0 to represent an unknown location.
///
/// Parameters:
///
/// - `filename` : The source file name.
/// - `line` : The 1-based line number, or 0 for unknown.
/// - `col` : The 1-based column number, or 0 for unknown.
///
/// Returns a new `Position` value.
pub fn Position::new(filename : String, line : Int, col : Int) -> Position {
  { filename, line, col }
}

///|
/// Returns the filename component of this position.
///
/// Returns the source file name string.
pub fn Position::filename(self : Position) -> String {
  self.filename
}

///|
/// Returns the 1-based line number, or 0 if the position is unknown.
///
/// Returns the line number as an `Int`.
pub fn Position::line(self : Position) -> Int {
  self.line
}

///|
/// Returns the 1-based column number, or 0 if the position is unknown.
///
/// Returns the column number as an `Int`.
pub fn Position::col(self : Position) -> Int {
  self.col
}

///|
/// Returns `true` if the position carries a meaningful line number.
///
/// Returns `true` when `line > 0`, `false` otherwise.
pub fn Position::is_valid(self : Position) -> Bool {
  self.line > 0
}

///|
/// Formats the position as `"file:line:col"`, `"file:line"`, or `"file"`,
/// depending on which fields are non-zero.
///
/// Returns the formatted position string.
pub fn Position::to_string(self : Position) -> String {
  if self.line > 0 {
    if self.col > 0 {
      "\{self.filename}:\{self.line}:\{self.col}"
    } else {
      "\{self.filename}:\{self.line}"
    }
  } else {
    self.filename
  }
}

///|
/// Returns `true` if this position precedes `other` in the same source file.
///
/// Parameters:
///
/// - `self` : The position to compare from.
/// - `other` : The position to compare against.
///
/// Returns `true` if `self` comes before `other` by line then column.
pub fn Position::is_before(self : Position, other : Position) -> Bool {
  if self.line != other.line {
    self.line < other.line
  } else {
    self.col < other.col
  }
}

///|
/// A parse-time error with a source position and a human-readable message.
pub struct SyntaxError {
  priv pos : Position
  priv msg : String
  priv kind : SyntaxErrorKind
}

///|
/// Classifies a `SyntaxError` so consumers can branch on the structural cause
/// instead of matching the English message text. `UnexpectedEof` and
/// `UnterminatedString` mark input that may become valid with more lines (used
/// by the REPL to request a continuation); every other error is `Other`.
pub(all) enum SyntaxErrorKind {
  UnexpectedEof
  UnterminatedString
  Other
} derive(Eq)

///|
/// Creates a `SyntaxError` at `pos` with message `msg` and kind `Other`.
///
/// Parameters:
///
/// - `pos` : The source position where the error was detected.
/// - `msg` : The human-readable error message.
///
/// Returns a new `SyntaxError` value.
pub fn SyntaxError::new(pos : Position, msg : String) -> SyntaxError {
  { pos, msg, kind: Other }
}

///|
/// Creates a `SyntaxError` at `pos` with message `msg` and an explicit `kind`.
///
/// Parameters:
///
/// - `pos` : The source position where the error was detected.
/// - `msg` : The human-readable error message.
/// - `kind` : The structural classification of the error.
///
/// Returns a new `SyntaxError` value.
pub fn SyntaxError::with_kind(
  pos : Position,
  msg : String,
  kind : SyntaxErrorKind,
) -> SyntaxError {
  { pos, msg, kind }
}

///|
/// Returns the source position where the syntax error was detected.
///
/// Returns the `Position` associated with this error.
pub fn SyntaxError::pos(self : SyntaxError) -> Position {
  self.pos
}

///|
/// Returns the human-readable error message.
///
/// Returns the error message string.
pub fn SyntaxError::msg(self : SyntaxError) -> String {
  self.msg
}

///|
/// Returns the structural classification of the error.
///
/// Returns the `SyntaxErrorKind` associated with this error.
pub fn SyntaxError::kind(self : SyntaxError) -> SyntaxErrorKind {
  self.kind
}

///|
/// Formats the error as `"pos: msg"`.
///
/// Returns the formatted error string.
pub fn SyntaxError::to_string(self : SyntaxError) -> String {
  "\{self.pos.to_string()}: \{self.msg}"
}

///|
/// A name-resolution error with a source position and a human-readable message.
pub struct ResolveError {
  priv pos : Position
  priv msg : String
}

///|
/// Creates a `ResolveError` at `pos` with message `msg`.
///
/// Parameters:
///
/// - `pos` : The source position where the error was detected.
/// - `msg` : The human-readable error message.
///
/// Returns a new `ResolveError` value.
pub fn ResolveError::new(pos : Position, msg : String) -> ResolveError {
  { pos, msg }
}

///|
/// Returns the source position where the resolve error was detected.
///
/// Returns the `Position` associated with this error.
pub fn ResolveError::pos(self : ResolveError) -> Position {
  self.pos
}

///|
/// Returns the human-readable error message.
///
/// Returns the error message string.
pub fn ResolveError::msg(self : ResolveError) -> String {
  self.msg
}

///|
/// Formats the error as `"pos: msg"`.
///
/// Returns the formatted error string.
pub fn ResolveError::to_string(self : ResolveError) -> String {
  "\{self.pos.to_string()}: \{self.msg}"
}

///|
/// A local variable name together with its definition position; used by the
/// debugger API.
pub struct Binding {
  priv name : String
  priv pos : Position
}

///|
/// Creates a `Binding` with the given name and definition position.
///
/// Parameters:
///
/// - `name` : The variable name.
/// - `pos` : The source position where the variable is defined.
///
/// Returns a new `Binding` value.
pub fn Binding::new(name : String, pos : Position) -> Binding {
  { name, pos }
}

///|
/// Returns the variable name.
///
/// Returns the name string of this binding.
pub fn Binding::name(self : Binding) -> String {
  self.name
}

///|
/// Returns the definition position of the variable.
///
/// Returns the `Position` where this variable is defined.
pub fn Binding::pos(self : Binding) -> Position {
  self.pos
}

///|
/// A single frame in a Starlark call stack: the function name and the source
/// position of the call site within that function.
pub struct CallFrame {
  priv name : String
  priv pos : Position
}

///|
/// Creates a `CallFrame` with the given function name and call-site position.
///
/// Parameters:
///
/// - `name` : The function name for this frame.
/// - `pos` : The source position of the call site within the function.
///
/// Returns a new `CallFrame` value.
pub fn CallFrame::new(name : String, pos : Position) -> CallFrame {
  { name, pos }
}

///|
/// Returns the function name for this frame.
///
/// Returns the function name string.
pub fn CallFrame::name(self : CallFrame) -> String {
  self.name
}

///|
/// Returns the source position recorded in this frame.
///
/// Returns the call-site `Position` for this frame.
pub fn CallFrame::pos(self : CallFrame) -> Position {
  self.pos
}

///|
/// An ordered snapshot of call frames, outermost first, innermost last.
pub struct CallStack {
  priv frames : Array[CallFrame]
}

///|
/// Creates a `CallStack` from a pre-built array of frames.
///
/// Parameters:
///
/// - `frames` : An array of `CallFrame` values, outermost first.
///
/// Returns a new `CallStack` wrapping the given frames.
pub fn CallStack::new(frames : Array[CallFrame]) -> CallStack {
  { frames, }
}

///|
/// Returns the number of frames in the stack.
///
/// Returns the frame count as an `Int`.
pub fn CallStack::length(self : CallStack) -> Int {
  self.frames.length()
}

///|
/// Returns the frame at index `i` (0 = outermost), or `None` if out of range.
///
/// Parameters:
///
/// - `self` : The call stack to index into.
/// - `i` : The zero-based index of the frame to retrieve.
///
/// Returns `Some(frame)` if `i` is in range, `None` otherwise.
pub fn CallStack::at(self : CallStack, i : Int) -> CallFrame? {
  if i < 0 || i >= self.frames.length() {
    None
  } else {
    Some(self.frames[i])
  }
}

///|
/// Removes and returns the innermost (last) frame, or `None` if the stack is
/// empty.
///
/// Returns `Some(frame)` with the removed innermost frame, or `None` if empty.
pub fn CallStack::pop(self : CallStack) -> CallFrame? {
  if self.frames.is_empty() {
    None
  } else {
    Some(self.frames.unsafe_pop())
  }
}

///|
/// Formats the stack as a Python-style traceback string, or `""` if empty.
///
/// Returns the formatted traceback string.
pub fn CallStack::to_string(self : CallStack) -> String {
  if self.frames.is_empty() {
    return ""
  }
  let buf = StringBuilder::new()
  buf.write_string("Traceback (most recent call last):\n")
  for frame in self.frames {
    buf.write_string("  \{frame.pos.to_string()}: in \{frame.name}\n")
  }
  buf.to_string()
}

///|
/// A runtime evaluation error carrying a message, a call-stack snapshot, and
/// an optional inner cause for load-error chaining.
pub struct EvalError {
  priv msg : String
  priv call_stack : CallStack
  priv cause : EvalError?
}

///|
/// Creates an `EvalError` with only a message and an empty call stack.
///
/// Parameters:
///
/// - `msg` : The human-readable error message.
///
/// Returns a new `EvalError` with an empty call stack and no cause.
pub fn EvalError::simple(msg : String) -> EvalError {
  { msg, call_stack: CallStack::new([]), cause: None }
}

///|
/// Creates an `EvalError` with a message and a call-stack snapshot.
///
/// Parameters:
///
/// - `msg` : The human-readable error message.
/// - `call_stack` : The call-stack snapshot at the point of the error.
///
/// Returns a new `EvalError` with the given message and stack, and no cause.
pub fn EvalError::with_stack(msg : String, call_stack : CallStack) -> EvalError {
  { msg, call_stack, cause: None }
}

///|
/// Creates an `EvalError` that wraps an inner `cause`, used to chain load
/// errors so the original inner backtrace is accessible.
///
/// Parameters:
///
/// - `msg` : The human-readable error message for the outer error.
/// - `call_stack` : The call-stack snapshot at the point of the outer error.
/// - `cause` : The inner `EvalError` being wrapped.
///
/// Returns a new `EvalError` that chains to the given cause.
pub fn EvalError::with_cause(
  msg : String,
  call_stack : CallStack,
  cause : EvalError,
) -> EvalError {
  { msg, call_stack, cause: Some(cause) }
}

///|
/// Returns the error message string.
///
/// Returns the message string of this error.
pub fn EvalError::msg(self : EvalError) -> String {
  self.msg
}

///|
/// Returns the call-stack snapshot captured when this error was raised.
///
/// Returns the `CallStack` recorded at the point of the error; empty for
/// errors created with `simple`.
pub fn EvalError::call_stack(self : EvalError) -> CallStack {
  CallStack::new(self.call_stack.frames.copy())
}

///|
/// Returns the inner cause error, if this error was created with
/// `with_cause`.
///
/// Returns `Some(inner)` if a cause was set, `None` otherwise.
pub fn EvalError::cause(self : EvalError) -> EvalError? {
  self.cause
}

///|
/// Returns just the error message (same as `msg`); implements the `Show`
/// trait.
///
/// Returns the error message string.
pub fn EvalError::to_string(self : EvalError) -> String {
  self.msg
}

///|
/// Returns a formatted traceback string ending with `"Error: msg"`. Strips a
/// trailing `` frame and appends it as `" in name"` instead.
///
/// Returns the full backtrace string including the error message.
pub fn EvalError::backtrace(self : EvalError) -> String {
  let frames = self.call_stack.frames
  let n = frames.length()
  let (stack_str, suffix) = if n > 0 &&
    frames[n - 1].pos.filename == "" {
    let name = frames[n - 1].name
    let trimmed : Array[CallFrame] = []
    for i in 0..<(n - 1) {
      trimmed.push(frames[i])
    }
    (CallStack::new(trimmed).to_string(), " in " + name)
  } else {
    (self.call_stack.to_string(), "")
  }
  "\{stack_str}Error\{suffix}: \{self.msg}"
}