// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: interpreter.go
// Source: debugger.go
// Copyright 2016 Google Inc. All rights reserved.
// Modifications: Implemented observation contracts using MoonBit callbacks and scoped value handles.
// License and retained attribution: LICENSE and NOTICE at the module root.
///|
/// Callbacks observe admitted expressions and their lazy results. Contexts and
/// value handles are valid only during the callback; copy node/range/text views
/// to retain them. Explicit lookups may force values under the execution budgets.
pub(all) struct EvalHook {
before : (EvalContext) -> Unit raise
after : (EvalContext, EvalOutcome) -> Unit raise
}
///|
pub(all) enum EvalOutcome {
Success(DebugValue)
Failure(Diagnostic)
}
///|
pub struct EvalContext {
priv vm : Vm
priv expression : Expr
priv environment : Env
priv active : Ref[Bool]
priv serial : Int
}
///|
pub struct DebugValue {
priv vm : Vm
priv value : Value
priv active : Ref[Bool]
priv location : Location
}
///|
priv struct EvalHookFrame {
expression : Expr
environment : Env
serial : Int
}
///|
fn EvalContext::check(self : EvalContext) -> Unit raise JsonnetError {
if !self.active.val {
raise problem(
Runtime,
self.expression.loc,
"evaluation context is no longer active",
)
}
}
///|
fn EvalContext::tick(self : EvalContext) -> Unit raise JsonnetError {
self.vm.tick(self.expression.loc) catch {
Error(d) => raise Error(d)
e => raise problem(Runtime, self.expression.loc, e.to_string())
}
}
///|
fn DebugValue::check(self : DebugValue) -> Unit raise JsonnetError {
if !self.active.val {
raise problem(Runtime, self.location, "debug value is no longer active")
}
}
///|
pub fn EvalContext::location(self : EvalContext) -> Location {
self.expression.loc
}
///|
/// Unique within one execution; before/after callbacks share the same ID.
pub fn EvalContext::evaluation_id(self : EvalContext) -> Int {
self.serial
}
///|
pub fn EvalContext::source_range(self : EvalContext) -> SourceRange? {
self.expression.end.map(end => { begin: self.expression.loc, end, })
}
///|
/// Return an independent semantic node without evaluating its children.
pub fn EvalContext::node(self : EvalContext) -> Ast raise JsonnetError {
self.check()
self.copy_node(self.expression)
}
///|
fn EvalContext::copy_node(
self : EvalContext,
root : Expr,
) -> Ast raise JsonnetError {
let work = [root]
while work.pop() is Some(node) {
self.tick()
for child in ast_export_children(node) {
work.push(child)
}
}
export_ast(root)
}
///|
/// List lexical identifiers without forcing their values. Sorted deterministically.
pub fn EvalContext::variables(
self : EvalContext,
) -> Array[String] raise JsonnetError {
self.check()
let names : Array[String] = []
let mut environment = Some(self.environment)
while environment is Some(env) {
self.tick()
if env.debug_visible is Some(visible) {
let captured = visible(() => self.vm.tick(self.expression.loc)) catch {
Error(d) => raise Error(d)
e => raise problem(Runtime, self.expression.loc, e.to_string())
}
for name in captured {
self.tick()
names.push(name)
}
break
}
for name, _ in env.names {
self.tick()
if name != "self" && name != "super" {
names.push(name)
}
}
environment = env.parent
}
names.sort_by(compare_unicode)
names
}
///|
/// Copy the active expression stack, outermost first. This semantic hook stack
/// also includes direct expressions and transparent local/conditional bodies.
pub fn EvalContext::stack(self : EvalContext) -> Array[Ast] raise JsonnetError {
self.check()
self.vm.hook_stack.map(frame => self.copy_node(frame.expression))
}
///|
/// Return a handle only when the named thunk is already cached; never force it.
pub fn EvalContext::peek(
self : EvalContext,
name : String,
) -> DebugValue? raise JsonnetError {
self.check()
if !self.variables().contains(name) && name != "self" && name != "super" {
return None
}
guard self.environment.lookup(if name == "$std" { "std" } else { name })
is Some(id) else {
return None
}
match self.vm.thunks[id] {
Ready(value) =>
Some({
vm: self.vm,
value,
active: self.active,
location: self.expression.loc,
})
_ => None
}
}
///|
/// Force only the named root value. Other hooks are suppressed while inspecting.
pub fn EvalContext::lookup(
self : EvalContext,
name : String,
) -> DebugValue raise JsonnetError {
self.check()
let loc = self.expression.loc
if !self.variables().contains(name) && name != "self" && name != "super" {
raise problem(Runtime, loc, "invalid identifier " + name)
}
guard self.environment.lookup(if name == "$std" { "std" } else { name })
is Some(id) else {
raise problem(Runtime, loc, "invalid identifier " + name)
}
let previous = self.vm.hooks_suppressed
self.vm.hooks_suppressed = true
defer {
self.vm.hooks_suppressed = previous
}
let value = self.vm.force(id, loc) catch {
Error(d) => raise Error(d)
e => raise problem(Runtime, loc, e.to_string())
}
{ vm: self.vm, value, active: self.active, location: loc, }
}
///|
/// Manifest an explicitly inspected value; this forces its children and may
/// execute assertions/imports. Inspection shares normal step/depth/stack budgets.
pub fn DebugValue::json(self : DebugValue) -> Json raise JsonnetError {
self.check()
let previous = self.vm.hooks_suppressed
self.vm.hooks_suppressed = true
defer {
self.vm.hooks_suppressed = previous
}
self.vm.manifest(self.value, self.location, 0) catch {
Error(d) => raise Error(d)
e => raise problem(Runtime, self.location, e.to_string())
}
}
///|
/// Scalar/function/container category without forcing children or assertions.
pub fn DebugValue::kind(self : DebugValue) -> String raise JsonnetError {
self.check()
match self.value {
Nil => "null"
Bool(_) => "boolean"
Num(_) => "number"
Str(_) => "string"
ArrayValue(_) => "array"
ObjectValue(_, _) => "object"
Closure(_, _, _) | Builtin(_) | Native(_) => "function"
}
}
///|
fn Vm::hook_before(
self : Vm,
expression : Expr,
environment : Env,
frames : Array[EvalHookFrame],
) -> Unit raise {
// These field values are runtime implementation nodes, not exportable source
// expressions. Compiler sugar may reach them while resolving its builtins.
if expression.kind is StandardFunction(_) ||
expression.kind is StandardValue(_) {
return
}
if !self.hooks_suppressed && self.eval_hook is Some(hook) {
let active = Ref(true)
let serial = self.hook_serial
self.hook_serial += 1
let frame : EvalHookFrame = { expression, environment, serial, }
let context : EvalContext = {
vm: self,
expression,
environment,
active,
serial,
}
self.hook_stack.push(frame)
errdefer ignore(self.hook_stack.pop())
self.hooks_suppressed = true
defer {
active.val = false
self.hooks_suppressed = false
}
(hook.before)(context)
frames.push(frame)
}
}
///|
fn Vm::hook_after(
self : Vm,
frames : Array[EvalHookFrame],
result : Value?,
error : Error?,
) -> Unit raise {
if frames.is_empty() {
return
}
guard self.eval_hook is Some(hook) else { return }
let base = self.hook_stack.length() - frames.length()
defer (while self.hook_stack.length() > base {
ignore(self.hook_stack.pop())
})
self.hooks_suppressed = true
defer {
self.hooks_suppressed = false
}
for i = frames.length() - 1; i >= 0; i = i - 1 {
let frame = frames[i]
if error is Some(_) && self.hook_failure_environment is None {
self.hook_failure_environment = Some(frame.environment)
}
let active = Ref(true)
defer {
active.val = false
}
let context : EvalContext = {
vm: self,
expression: frame.expression,
environment: if error is Some(_) {
self.hook_failure_environment.unwrap()
} else {
frame.environment
},
active,
serial: frame.serial,
}
let outcome = match (result, error) {
(Some(value), _) =>
Success({ vm: self, value, active, location: frame.expression.loc, })
(_, Some(Error(d))) =>
Failure({ ..d, context: d.context.copy(), trace: d.trace.copy(), })
(_, Some(e)) =>
Failure({
kind: Runtime,
message: e.to_string(),
location: frame.expression.loc,
context: [],
trace: [],
})
_ => abort("missing evaluation hook outcome")
}
self.trace_expression(frame.expression, frame.environment)
(hook.after)(context, outcome) catch {
Error(d) => raise Error(self.traced(d))
e =>
raise self.trace_problem(Runtime, frame.expression.loc, e.to_string())
}
ignore(self.hook_stack.pop())
}
}
///|
/// Hook replacement preserves import caches, like trace replacement. Reentrant
/// configuration changes are rejected. Omit the callback to disable hooks.
pub fn Evaluator::set_eval_hook(
self : Evaluator,
hook? : EvalHook,
) -> Unit raise JsonnetError {
self.idle()
self.eval_hook = hook
}