// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: interpreter.go
// Source: runtime_error.go
// Source: internal/parser/context.go
// Source: internal/program/desugarer.go
// Copyright 2016 Google Inc. All rights reserved.
// Copyright 2017 Google Inc. All rights reserved.
// Modifications: Adapted named contexts, clean frames and synthetic trace sites to MoonBit execution state.
// License and retained attribution: LICENSE and NOTICE at the module root.

///|
priv struct RuntimeFrame {
  trimmable : Bool
  trace : TraceFrame?
}

///|
// Assert sugar intentionally lacks a public semantic range. Its diagnostic
// failure site covers the original assertion through its final continuation.
fn diagnostic_expr_end(root : Expr) -> Location {
  let mut node = root
  while node.end is None {
    match node.kind {
      Assert(_, _, body) => node = body
      _ => return node.loc
    }
  }
  node.end.unwrap()
}

///|
/// Preserve original object-assert declaration spans when exporting an editable
/// semantic AST. Keys are the condition's source-range identity. The result is
/// independent; pass it to from_ast after edits that retain those identities.
pub fn Program::assertion_ranges(self : Program) -> Map[String, SourceRange] {
  let ranges : Map[String, SourceRange] = Map([])
  let work = [self.expression]
  while work.pop() is Some(node) {
    if node.kind is Obj(_, _, checks, sites) {
      for index, (condition, _) in checks {
        if sites.get(index).bind(site => site) is Some(range) {
          let key = (
            {
              begin: condition.loc,
              end: condition.end.unwrap_or(condition.loc),
            } : SourceRange).format()
          ranges[key] = range
        }
      }
    }
    for child in ast_export_children(node) {
      work.push(child)
    }
  }
  ranges
}

///|
fn Env::trace_context(
  self : Env,
  name : String,
  bound? : String = "anonymous",
) -> Env {
  {
    ..self,
    diagnostic_context: if self.diagnostic_suppressed {
      ("", "anonymous")
    } else {
      (name, bound)
    },
  }
}

///|
fn Env::trace_thunk(self : Env) -> Env {
  self.trace_context("thunk from <" + self.diagnostic_context.0 + ">")
}

///|
fn Vm::trace_expression(
  self : Vm,
  expression : Expr,
  environment : Env,
) -> Unit {
  self.current_trace = Some({
    location: Source({
      begin: expression.loc,
      end: expression.end.unwrap_or(expression.loc),
    }),
    name: environment.diagnostic_context.0,
  })
}

///|
// Snapshot before any execution frame is removed. Existing snapshots (including
// retained assertion errors) are not extended or shared with a later caller.
fn Vm::traced(self : Vm, diagnostic : Diagnostic) -> Diagnostic {
  if !diagnostic.trace.is_empty() ||
    diagnostic.kind == Syntax ||
    diagnostic.kind == Static {
    return diagnostic
  }
  let trace : Array[TraceFrame] = []
  for frame in self.frames {
    if frame.trace is Some(frame) {
      trace.push(frame)
    }
  }
  if self.current_trace is Some(frame) {
    trace.push(frame)
  }
  { ..diagnostic, trace, }
}

///|
fn Vm::trace_problem(
  self : Vm,
  kind : ErrorKind,
  loc : Location,
  message : String,
) -> JsonnetError {
  Error(self.traced({ kind, location: loc, message, context: [], trace: [], }))
}

///|
fn Vm::pop_frame(self : Vm) -> Unit {
  if self.frames.pop() is Some(frame) {
    self.current_trace = frame.trace
  }
}

///|
fn[T] Vm::with_trace(
  self : Vm,
  location : TraceLocation,
  body : () -> T raise,
) -> T raise {
  let previous = self.current_trace
  self.current_trace = Some({ location, name: "", })
  defer {
    self.current_trace = previous
  }
  body() catch {
    Error(d) => raise Error(self.traced(d))
    e => raise e
  }
}