// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: value.go
// Source: thunks.go
// Source: interpreter.go
// Source: runtime_error.go
// Copyright 2017 Google Inc. All rights reserved.
// Copyright 2016 Google Inc. All rights reserved.
// Modifications: Adapted repeated parameter binding, lazy evaluation observation and trace contracts to the MoonBit evaluator and its budgets.
// License and retained attribution: LICENSE and NOTICE at the module root.
///|
fn Vm::eval(
self : Vm,
expr : Expr,
env : Env,
tail? : Bool = false,
preserve_binding? : Bool = false,
) -> Value raise {
self.tick(expr.loc)
if self.depth >= self.limits.depth {
raise self.trace_problem(Limit, expr.loc, "evaluation depth limit exceeded")
}
self.depth += 1
defer {
self.depth -= 1
}
self.eval_inner(
expr,
if preserve_binding {
env
} else {
env.trace_context(env.diagnostic_context.0)
},
tail~,
)
}
///|
fn Vm::eval_inner(
self : Vm,
expr : Expr,
env : Env,
tail~ : Bool,
) -> Value raise {
let previous_trace = self.current_trace
defer {
self.current_trace = previous_trace
}
let events : Array[EvalHookFrame] = []
let forwarded : Array[(Int, Location, Bool)] = []
let result = self.eval_loop(expr, env, tail~, events~, forwarded~) catch {
Error(d) => {
for i = forwarded.length() - 1; i >= 0; i = i - 1 {
d.context.push(forwarded[i].1)
}
let d = self.traced(d)
self.hook_after(events, None, Some(Error(d)))
for (id, _, _) in forwarded {
self.thunks[id] = Failed(Error(d))
}
raise Error(d)
}
error => {
self.hook_after(events, None, Some(error))
raise error
}
}
self.hook_after(events, Some(result), None)
for (id, _, owner) in forwarded {
self.thunks[id] = Ready(result)
if owner {
ignore(self.retries.remove(id))
}
}
result
}
///|
fn Vm::eval_loop(
self : Vm,
expr : Expr,
env : Env,
tail~ : Bool,
events~ : Array[EvalHookFrame],
forwarded~ : Array[(Int, Location, Bool)],
) -> Value raise {
let mut expr = expr
let mut env = env
let mut tail = tail
let frame_base = self.frames.length()
// The loop owns any strict function frame added above its entry frame.
defer (while self.frames.length() > frame_base {
self.pop_frame()
})
while true {
if !self.hooks_suppressed &&
self.eval_hook is Some(_) &&
(expr.kind is ArrayComp(_, _) || expr.kind is ObjectComp(_, _, _, _)) {
expr = debugger_lower_comprehension(expr)
}
self.trace_expression(expr, env)
self.tick(expr.loc)
self.hook_before(expr, env, events) catch {
Error(d) => raise Error(self.traced(d))
e => raise self.trace_problem(Runtime, expr.loc, e.to_string())
}
let loc = expr.loc
return try {
match expr.kind {
NumberLiteral(raw) => {
let n = @string.parse_double(raw) catch {
_ => raise problem(Runtime, loc, "overflow")
}
Num(n)
}
Literal(j) => self.from_json(j, loc)
Var(n) =>
match env.lookup(n) {
Some(t) => self.force(t, loc)
None => raise problem(Static, loc, "unknown variable '\{n}'")
}
Local(bs, body) => {
env = self.bind(bs, env, debug_expression=expr)
expr = body
continue
}
If(c, yes, no) => {
expr = if boolean(self.eval(c, env), c.loc) { yes } else { no }
env = env.trace_context(env.diagnostic_context.0)
continue
}
Func(ps, body) => Closure(ps, body, env)
Call(f, args, strict) => {
let function = self.eval(f, env)
let lowered_call = f.end is None &&
f.kind
is Index(
{ kind: Var("$std"), .. },
{ kind: Literal(String(_)), .. }
)
let thunks = args.map(pair => {
(
pair.0,
self.store(
Pending(
pair.1,
env
.debug_capture(if lowered_call { [expr] } else { [pair.1] })
.trace_thunk(),
),
),
)
})
if tail &&
self.frames.last().map(frame => frame.trimmable) == Some(true) {
ignore(self.frames.pop())
}
if tail && function is Builtin("extVar") {
self.tick(loc)
let id = try {
let bound = bind_arguments([("x", true)], thunks, loc)
self.external_parameter(self.force(bound["x"], loc), loc)
} catch {
Error(d) => {
d.context.push(loc)
raise Error(d)
}
error => raise error
}
let owner = self.thunks[id] is Code(_, _) ||
self.thunks[id] is ParsedParameter(_, _)
if self.forward_parameter(id) is Some((next, scope)) {
forwarded.push((id, loc, owner))
self.push_frame(next.loc, false)
if self.depth >= self.limits.depth {
raise self.trace_problem(
Limit,
next.loc,
"evaluation depth limit exceeded",
)
}
self.tick(next.loc)
expr = next
env = scope
continue
}
return self.force(id, loc)
}
if strict && function is Closure(params, body, outer) {
let scope = try {
let scope = self.call_env(params, thunks, outer, loc, body~)
for { name: n, .. } in params {
ignore(self.force(scope.names[n], loc))
}
scope
} catch {
Error(d) => {
d.context.push(loc)
raise Error(d)
}
e => raise e
}
expr = body
env = scope
self.push_frame(loc, true)
tail = true
continue
}
self.call(function, thunks, loc, strict~) catch {
Error(d) => {
d.context.push(loc)
raise Error(d)
}
e => raise e
}
}
Arr(items) =>
ArrayValue(
items.map(e => {
self.store(Pending(e, env.debug_capture([e]).trace_thunk()))
}),
)
Obj(fields, locals, assertions, assertion_sites) => {
let captured = env.debug_capture([expr])
let fs : Map[String, LayerField] = Map([])
for f in fields {
match self.eval(f.key, env) {
Nil => ()
Str(k) => {
if fs.contains(k) {
raise problem(Runtime, f.key.loc, "duplicate field '\{k}'")
}
fs[k] = { field: f, env: captured, }
}
_ =>
raise problem(
Type,
f.key.loc,
"field name must be string or null",
)
}
}
self.object_value([
{ fields: fs, locals, assertions, assertion_sites, env: captured, },
])
}
ArrayComp(body, clauses) => {
let out = []
self.comprehend_expression(expr, clauses, env, scope => {
out.push(
self.store(
Pending(body, scope.debug_capture([body]).trace_thunk()),
),
)
})
ArrayValue(out)
}
ObjectComp(key, body, clauses, locals) => {
let fs : Map[String, LayerField] = Map([])
self.comprehend_expression(expr, clauses, env, scope => {
match self.eval(key, scope) {
Nil => ()
Str(k) => {
if fs.contains(k) {
raise problem(Runtime, key.loc, "duplicate field '\{k}'")
}
fs[k] = {
field: {
key,
value: {
kind: Local(locals, body),
loc: body.loc,
end: body.end,
},
visibility: 1,
additive: false,
},
env: scope,
}
}
_ =>
raise problem(
Type,
key.loc,
"field name must be string or null",
)
}
})
self.object_value([
{
fields: fs,
locals: [],
assertions: [],
assertion_sites: [],
env,
},
])
}
Index(a, b) => {
// The reference annotates lexical contexts before desugaring dot
// access. Its original dot node has no direct children, leaving the
// target subtree unannotated. Computed indexes retain their context.
let scope = if b.end is None && b.kind is Literal(String(_)) {
{
..env,
diagnostic_context: ("", "anonymous"),
diagnostic_suppressed: true,
}
} else {
env
}
self.index(self.eval(a, scope), self.eval(b, scope), loc)
}
Slice(a, start, end, step) => {
self.current_trace = Some({
location: Source({
begin: expr.loc,
end: diagnostic_expr_end(expr),
}),
name: "",
})
let library = self.force(env.lookup("$std").unwrap(), loc)
let function = self.index(library, Str("slice"), loc)
let argument_env = env.trace_context("")
let arguments = [Some(a), start, end, step].map(expression => {
let thunk = match expression {
Some(expression) =>
self.store(
Pending(expression, argument_env.debug_capture([expression])),
)
None => self.store(Ready(Nil))
}
(None, thunk)
})
self.call(function, arguments, loc)
}
Unary(op, e) => {
let v = self.eval(e, env)
match op {
"!" => Bool(!boolean(v, loc))
"-" => Num(-number_value(v, loc))
"+" => Num(number_value(v, loc))
_ => Num(reference_int64(number_value(v, loc)).lnot().to_double())
}
}
Binary(op, a, b) => {
if op == "%" {
let library = self.force(env.lookup("$std").unwrap(), loc)
let function = self.index(library, Str("mod"), loc)
// Go assigns lexical contexts before lowering %. The new call has
// no context; its original operands retain theirs without adding
// the thunk context of an explicit source call.
self.current_trace = Some({
location: Source({
begin: expr.loc,
end: diagnostic_expr_end(expr),
}),
name: "",
})
return self.call(
function,
[a, b].map(arg => {
(None, self.store(Pending(arg, env.debug_capture([expr]))))
}),
loc,
)
}
let left = self.eval(a, env)
if op == "&&" {
Bool(boolean(left, loc) && boolean(self.eval(b, env), loc))
} else if op == "||" {
Bool(boolean(left, loc) || boolean(self.eval(b, env), loc))
} else {
self.binary(op, left, self.eval(b, env), loc)
}
}
Assert(c, m, body) => {
self.assertion(
c,
m,
env,
condition_frame={ location: Synthetic(""), name: "", },
failure_frame={
location: Source({
begin: expr.loc,
end: diagnostic_expr_end(expr),
}),
name: "",
},
)
expr = body
env = env.trace_context(env.diagnostic_context.0)
continue
}
Fail(e) =>
raise problem(
Runtime,
loc,
self.coerce_string(self.eval(e, env), loc),
)
ImportFile(path, raw) =>
self.import_file(path, raw, env.import_from, loc)
ImportBinary(path) => self.import_binary(path, env.import_from, loc)
StandardFunction(name) => Builtin(name)
StandardValue(json) => self.from_json(json, loc)
}
} catch {
Error(d) => raise Error(self.traced(d))
e => raise self.trace_problem(Runtime, loc, e.to_string())
}
} nobreak {
abort("unreachable evaluator loop")
}
}
///|
fn Vm::comprehend_expression(
self : Vm,
expression : Expr,
clauses : Array[Clause],
environment : Env,
emit : (Env) -> Unit raise,
) -> Unit raise {
// Go lowers a comprehension to a call at the whole expression's range.
// Its input/condition/key evaluation has a clean frame under that call.
self.with_trace(
Source({ begin: expression.loc, end: diagnostic_expr_end(expression), }),
() => {
self.push_frame(expression.loc, false)
defer self.pop_frame()
self.comprehend(clauses, 0, environment, expression.loc, emit) catch {
Error(d) => raise Error(self.traced(d))
e => raise e
}
},
)
}
///|
fn Vm::comprehend(
self : Vm,
clauses : Array[Clause],
index : Int,
env : Env,
loc : Location,
emit : (Env) -> Unit raise,
) -> Unit raise {
self.tick(loc)
if index >= self.limits.depth {
raise problem(Limit, loc, "comprehension depth limit exceeded")
}
if index == clauses.length() {
emit(env)
return
}
match clauses[index] {
For(n, expr) => {
let a = array_value(self.eval(expr, env), expr.loc)
for t in a {
let scope = env.child()
scope.names[n] = t
self.comprehend(clauses, index + 1, scope, loc, emit)
}
}
When(expr) =>
if boolean(self.eval(expr, env), expr.loc) {
self.comprehend(clauses, index + 1, env, loc, emit)
}
}
}
///|
fn Vm::call(
self : Vm,
function : Value,
args : Array[(String?, Int)],
loc : Location,
strict? : Bool = false,
) -> Value raise {
self.tick(loc)
match function {
Native(f) => self.call_native(f, args, loc)
Builtin(name) => self.builtin(name, args, loc)
Closure(params, body, outer) => {
let env = self.call_env(params, args, outer, loc, body~)
if strict {
for { name: n, .. } in params {
ignore(self.force(env.names[n], loc))
}
}
self.eval_frame(body, env, trimmable=strict)
}
_ =>
raise problem(
Type,
loc,
"Unexpected type " + value_type(function) + ", expected function",
)
}
}
///|
fn Vm::call_env(
self : Vm,
params : Array[ExprParameter],
args : Array[(String?, Int)],
outer : Env,
loc : Location,
body? : Expr,
) -> Env raise {
let roots = params.filter_map(p => p.default_value)
if body is Some(body) {
roots.push(body)
}
let env = outer
.debug_capture(roots, bound=params.map(p => p.name))
.child()
.trace_context("function <" + outer.diagnostic_context.1 + ">")
let assigned = bind_arguments(
params.map(p => (p.name, p.default_value is None)),
args,
loc,
)
for name, thunk in assigned {
env.names[name] = thunk
}
for { name: n, default_value: d, .. } in params {
if !env.names.contains(n) && d is Some(e) {
env.names[n] = self.store(Pending(e, env))
}
}
env
}
///|
fn Vm::index(
self : Vm,
target : Value,
key : Value,
loc : Location,
) -> Value raise {
match target {
ObjectValue(id, top) => self.field(id, top, text_value(key, loc), loc)
ArrayValue(a) => {
let i = array_index(key, a.length(), loc)
self.force(a[i], loc)
}
Str(s) => {
let chars = s.to_array()
let i = array_index(key, chars.length(), loc)
Str(chars[i].to_string())
}
_ =>
raise problem(
Type,
loc,
"Value non indexable: *jsonnet.value" +
(match target {
Nil => "Null"
Bool(_) => "Boolean"
Num(_) => "Number"
Closure(_, _, _) | Builtin(_) | Native(_) => "Function"
_ => abort("indexable value handled above")
}),
)
}
}
///|
fn array_index(key : Value, length : Int, loc : Location) -> Int raise {
let number = number_value(key, loc)
let n = reference_int64(number)
if n < 0L || n >= length.to_int64() {
raise problem(
Runtime,
loc,
"Index \{n} out of bounds, not within [0, \{length})",
)
}
n.to_int()
}