mirror of
https://github.com/google/go-jsonnet.git
synced 2025-08-07 14:57:24 +02:00
This change adds caching to objects fields, i.e. now subsequent references to an object field are going to be served from cache. Cache is kept within an object. Objects created with operator + start with a clean cache (they have to, because in general all the fields may have changed their values due to late binding). This change comes naturally with a change of structure of objects, now valueObject is a concrete struct which keeps "uncachedObject" which is roughly equivalent to old objects.
1270 lines
32 KiB
Go
1270 lines
32 KiB
Go
/*
|
|
Copyright 2016 Google Inc. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package jsonnet
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
|
|
"github.com/google/go-jsonnet/ast"
|
|
)
|
|
|
|
// TODO(sbarzowski) use it as a pointer in most places b/c it can sometimes be shared
|
|
// for example it can be shared between array elements and function arguments
|
|
type environment struct {
|
|
selfBinding selfBinding
|
|
|
|
// Bindings introduced in this frame. The way previous bindings are treated
|
|
// depends on the type of a frame.
|
|
// If isCall == true then previous bindings are ignored (it's a clean
|
|
// environment with just the variables we have here).
|
|
// If isCall == false then if this frame doesn't contain a binding
|
|
// previous bindings will be used.
|
|
upValues bindingFrame
|
|
}
|
|
|
|
func makeEnvironment(upValues bindingFrame, sb selfBinding) environment {
|
|
return environment{
|
|
upValues: upValues,
|
|
selfBinding: sb,
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) getCurrentStackTrace(additional TraceElement) []TraceFrame {
|
|
var result []TraceFrame
|
|
for _, f := range i.stack.stack {
|
|
if f.isCall {
|
|
result = append(result, traceElementToTraceFrame(f.trace))
|
|
}
|
|
}
|
|
if additional.loc != nil {
|
|
result = append(result, traceElementToTraceFrame(additional))
|
|
}
|
|
return result
|
|
}
|
|
|
|
type callFrame struct {
|
|
// True if it switches to a clean environment (function call or array element)
|
|
// False otherwise, e.g. for local
|
|
// This makes callFrame a misnomer as it is technically not always a call...
|
|
isCall bool
|
|
|
|
// Tracing information about the place where it was called from.
|
|
trace TraceElement
|
|
|
|
// Whether this frame can be removed from the stack when it doesn't affect
|
|
// the evaluation result, but in case of an error, it won't appear on the
|
|
// stack trace.
|
|
// It's used for tail call optimization.
|
|
trimmable bool
|
|
|
|
env environment
|
|
}
|
|
|
|
func dumpCallFrame(c *callFrame) string {
|
|
var loc ast.LocationRange
|
|
if c.trace.loc == nil {
|
|
loc = ast.MakeLocationRangeMessage("?")
|
|
} else {
|
|
loc = *c.trace.loc
|
|
}
|
|
return fmt.Sprintf("<callFrame isCall = %t location = %v trimmable = %t>",
|
|
c.isCall,
|
|
loc,
|
|
c.trimmable,
|
|
)
|
|
}
|
|
|
|
type callStack struct {
|
|
calls int
|
|
limit int
|
|
stack []*callFrame
|
|
}
|
|
|
|
func dumpCallStack(c *callStack) string {
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "<callStack calls = %d limit = %d stack:\n", c.calls, c.limit)
|
|
for _, callFrame := range c.stack {
|
|
fmt.Fprintf(&buf, " %v\n", dumpCallFrame(callFrame))
|
|
}
|
|
buf.WriteString("\n>")
|
|
return buf.String()
|
|
}
|
|
|
|
func (s *callStack) top() *callFrame {
|
|
r := s.stack[len(s.stack)-1]
|
|
return r
|
|
}
|
|
|
|
// It might've been popped already by tail call optimization.
|
|
// We check if it was trimmed by comparing the current stack size to the position
|
|
// of the frame we want to pop.
|
|
func (s *callStack) popIfExists(whichFrame int) {
|
|
if len(s.stack) == whichFrame {
|
|
if s.top().isCall {
|
|
s.calls--
|
|
}
|
|
s.stack = s.stack[:len(s.stack)-1]
|
|
}
|
|
}
|
|
|
|
/** If there is a trimmable frame followed by some locals, pop them all. */
|
|
func (s *callStack) tailCallTrimStack() {
|
|
for i := len(s.stack) - 1; i >= 0; i-- {
|
|
if s.stack[i].isCall {
|
|
if !s.stack[i].trimmable {
|
|
return
|
|
}
|
|
// Remove this stack frame and everything above it
|
|
s.stack = s.stack[:i]
|
|
s.calls--
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type tailCallStatus int
|
|
|
|
const (
|
|
nonTailCall tailCallStatus = iota
|
|
tailCall
|
|
)
|
|
|
|
func (s *callStack) newCall(trace TraceElement, env environment, trimmable bool) {
|
|
s.stack = append(s.stack, &callFrame{
|
|
isCall: true,
|
|
trace: trace,
|
|
env: env,
|
|
trimmable: trimmable,
|
|
})
|
|
s.calls++
|
|
}
|
|
|
|
func (s *callStack) newLocal(vars bindingFrame) {
|
|
s.stack = append(s.stack, &callFrame{
|
|
env: makeEnvironment(vars, selfBinding{}),
|
|
})
|
|
}
|
|
|
|
// getSelfBinding resolves the self construct
|
|
func (s *callStack) getSelfBinding() selfBinding {
|
|
for i := len(s.stack) - 1; i >= 0; i-- {
|
|
if s.stack[i].isCall {
|
|
return s.stack[i].env.selfBinding
|
|
}
|
|
}
|
|
panic(fmt.Sprintf("malformed stack %v", dumpCallStack(s)))
|
|
}
|
|
|
|
// lookUpVar finds for the closest variable in scope that matches the given name.
|
|
func (s *callStack) lookUpVar(id ast.Identifier) *cachedThunk {
|
|
for i := len(s.stack) - 1; i >= 0; i-- {
|
|
bind, present := s.stack[i].env.upValues[id]
|
|
if present {
|
|
return bind
|
|
}
|
|
if s.stack[i].isCall {
|
|
// Nothing beyond the captured environment of the thunk / closure.
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *callStack) lookUpVarOrPanic(id ast.Identifier) *cachedThunk {
|
|
th := s.lookUpVar(id)
|
|
if th == nil {
|
|
panic(fmt.Sprintf("RUNTIME: Unknown variable: %v (we should have caught this statically)", id))
|
|
}
|
|
return th
|
|
}
|
|
|
|
func (s *callStack) getCurrentEnv(ast ast.Node) environment {
|
|
return makeEnvironment(
|
|
s.capture(ast.FreeVariables()),
|
|
s.getSelfBinding(),
|
|
)
|
|
}
|
|
|
|
func (s *callStack) getTopEnv() environment {
|
|
top := s.stack[len(s.stack)-1]
|
|
if !top.isCall {
|
|
panic("getTopEnv is allowed only for artifical nodes which are called in new environment")
|
|
}
|
|
return top.env
|
|
}
|
|
|
|
// Build a binding frame containing specified variables.
|
|
func (s *callStack) capture(freeVars ast.Identifiers) bindingFrame {
|
|
env := make(bindingFrame)
|
|
for _, fv := range freeVars {
|
|
env[fv] = s.lookUpVarOrPanic(fv)
|
|
}
|
|
return env
|
|
}
|
|
|
|
func makeCallStack(limit int) callStack {
|
|
return callStack{
|
|
calls: 0,
|
|
limit: limit,
|
|
}
|
|
}
|
|
|
|
// Keeps current execution context and evaluates things
|
|
type interpreter struct {
|
|
// Current stack. It is used for:
|
|
// 1) Keeping environment (object we're in, variables)
|
|
// 2) Diagnostic information in case of failure
|
|
stack callStack
|
|
|
|
// External variables
|
|
extVars map[string]*cachedThunk
|
|
|
|
// Native functions
|
|
nativeFuncs map[string]*NativeFunction
|
|
|
|
// A part of std object common to all files
|
|
baseStd *valueObject
|
|
|
|
// Keeps imports
|
|
importCache *ImportCache
|
|
}
|
|
|
|
// Map union, b takes precedence when keys collide.
|
|
func addBindings(a, b bindingFrame) bindingFrame {
|
|
result := make(bindingFrame)
|
|
|
|
for k, v := range a {
|
|
result[k] = v
|
|
}
|
|
|
|
for k, v := range b {
|
|
result[k] = v
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (i *interpreter) newCall(trace TraceElement, env environment, trimmable bool) error {
|
|
s := &i.stack
|
|
if s.calls >= s.limit {
|
|
return makeRuntimeError("max stack frames exceeded.", i.getCurrentStackTrace(trace))
|
|
}
|
|
s.newCall(trace, env, trimmable)
|
|
return nil
|
|
}
|
|
|
|
func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) {
|
|
trace := TraceElement{
|
|
loc: a.Loc(),
|
|
context: a.Context(),
|
|
}
|
|
|
|
switch node := a.(type) {
|
|
case *ast.Array:
|
|
sb := i.stack.getSelfBinding()
|
|
var elements []*cachedThunk
|
|
for _, el := range node.Elements {
|
|
env := makeEnvironment(i.stack.capture(el.FreeVariables()), sb)
|
|
elThunk := cachedThunk{env: &env, body: el}
|
|
elements = append(elements, &elThunk)
|
|
}
|
|
return makeValueArray(elements), nil
|
|
|
|
case *ast.Binary:
|
|
if node.Op == ast.BopAnd {
|
|
// Special case for shortcut semantics.
|
|
xv, err := i.evaluate(node.Left, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
x, err := i.getBoolean(xv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !x.value {
|
|
return x, nil
|
|
}
|
|
yv, err := i.evaluate(node.Right, tc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getBoolean(yv, trace)
|
|
} else if node.Op == ast.BopOr {
|
|
// Special case for shortcut semantics.
|
|
xv, err := i.evaluate(node.Left, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
x, err := i.getBoolean(xv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if x.value {
|
|
return x, nil
|
|
}
|
|
yv, err := i.evaluate(node.Right, tc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getBoolean(yv, trace)
|
|
|
|
} else {
|
|
left, err := i.evaluate(node.Left, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
right, err := i.evaluate(node.Right, tc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO(dcunnin): The double dereference here is probably not necessary.
|
|
builtin := bopBuiltins[node.Op]
|
|
return builtin.function(i, trace, left, right)
|
|
}
|
|
|
|
case *ast.Unary:
|
|
value, err := i.evaluate(node.Expr, tc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
builtin := uopBuiltins[node.Op]
|
|
|
|
result, err := builtin.function(i, trace, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
|
|
case *ast.Conditional:
|
|
cond, err := i.evaluate(node.Cond, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
condBool, err := i.getBoolean(cond, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if condBool.value {
|
|
return i.evaluate(node.BranchTrue, tc)
|
|
}
|
|
return i.evaluate(node.BranchFalse, tc)
|
|
|
|
case *ast.DesugaredObject:
|
|
// Evaluate all the field names. Check for null, dups, etc.
|
|
fields := make(simpleObjectFieldMap)
|
|
for _, field := range node.Fields {
|
|
fieldNameValue, err := i.evaluate(field.Name, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var fieldName string
|
|
switch fieldNameValue := fieldNameValue.(type) {
|
|
case *valueString:
|
|
fieldName = fieldNameValue.getString()
|
|
case *valueNull:
|
|
// Omitted field.
|
|
continue
|
|
default:
|
|
return nil, i.Error(fmt.Sprintf("Field name must be string, got %v", fieldNameValue.getType().name), trace)
|
|
}
|
|
|
|
if _, ok := fields[fieldName]; ok {
|
|
return nil, i.Error(duplicateFieldNameErrMsg(fieldName), trace)
|
|
}
|
|
var f unboundField = &codeUnboundField{field.Body}
|
|
if field.PlusSuper {
|
|
f = &PlusSuperUnboundField{f}
|
|
}
|
|
fields[fieldName] = simpleObjectField{field.Hide, f}
|
|
}
|
|
var asserts []unboundField
|
|
for _, assert := range node.Asserts {
|
|
asserts = append(asserts, &codeUnboundField{assert})
|
|
}
|
|
var locals []objectLocal
|
|
for _, local := range node.Locals {
|
|
locals = append(locals, objectLocal{name: local.Variable, node: local.Body})
|
|
}
|
|
upValues := i.stack.capture(node.FreeVariables())
|
|
return makeValueSimpleObject(upValues, fields, asserts, locals), nil
|
|
|
|
case *ast.Error:
|
|
msgVal, err := i.evaluate(node.Expr, nonTailCall)
|
|
if err != nil {
|
|
// error when evaluating error message
|
|
return nil, err
|
|
}
|
|
if msgVal.getType() != stringType {
|
|
msgVal, err = builtinToString(i, trace, msgVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
msg, err := i.getString(msgVal, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, i.Error(msg.getString(), trace)
|
|
|
|
case *ast.Index:
|
|
targetValue, err := i.evaluate(node.Target, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index, err := i.evaluate(node.Index, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch target := targetValue.(type) {
|
|
case *valueObject:
|
|
indexString, err := i.getString(index, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return target.index(i, trace, indexString.getString())
|
|
case *valueArray:
|
|
indexInt, err := i.getNumber(index, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO(https://github.com/google/jsonnet/issues/377): non-integer indexes should be an error
|
|
return target.index(i, trace, int(indexInt.value))
|
|
|
|
case *valueString:
|
|
indexInt, err := i.getNumber(index, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO(https://github.com/google/jsonnet/issues/377): non-integer indexes should be an error
|
|
return target.index(i, trace, int(indexInt.value))
|
|
}
|
|
|
|
return nil, i.Error(fmt.Sprintf("Value non indexable: %v", reflect.TypeOf(targetValue)), trace)
|
|
|
|
case *ast.Import:
|
|
codePath := node.Loc().FileName
|
|
return i.importCache.ImportCode(codePath, node.File.Value, i, trace)
|
|
|
|
case *ast.ImportStr:
|
|
codePath := node.Loc().FileName
|
|
return i.importCache.ImportString(codePath, node.File.Value, i, trace)
|
|
|
|
case *ast.LiteralBoolean:
|
|
return makeValueBoolean(node.Value), nil
|
|
|
|
case *ast.LiteralNull:
|
|
return makeValueNull(), nil
|
|
|
|
case *ast.LiteralNumber:
|
|
return makeValueNumber(node.Value), nil
|
|
|
|
case *ast.LiteralString:
|
|
return makeValueString(node.Value), nil
|
|
|
|
case *ast.Local:
|
|
vars := make(bindingFrame)
|
|
bindEnv := i.stack.getCurrentEnv(a)
|
|
for _, bind := range node.Binds {
|
|
th := cachedThunk{env: &bindEnv, body: bind.Body}
|
|
|
|
// recursive locals
|
|
vars[bind.Variable] = &th
|
|
bindEnv.upValues[bind.Variable] = &th
|
|
}
|
|
i.stack.newLocal(vars)
|
|
sz := len(i.stack.stack)
|
|
// Add new stack frame, with new thunk for this variable
|
|
// execute body WRT stack frame.
|
|
v, err := i.evaluate(node.Body, tc)
|
|
i.stack.popIfExists(sz)
|
|
|
|
return v, err
|
|
|
|
case *ast.Self:
|
|
sb := i.stack.getSelfBinding()
|
|
return sb.self, nil
|
|
|
|
case *ast.Var:
|
|
foo := i.stack.lookUpVarOrPanic(node.Id)
|
|
return foo.getValue(i, trace)
|
|
|
|
case *ast.SuperIndex:
|
|
index, err := i.evaluate(node.Index, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
indexStr, err := i.getString(index, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return objectIndex(i, trace, i.stack.getSelfBinding().super(), indexStr.getString())
|
|
|
|
case *ast.InSuper:
|
|
index, err := i.evaluate(node.Index, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
indexStr, err := i.getString(index, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hasField := objectHasField(i.stack.getSelfBinding().super(), indexStr.getString(), withHidden)
|
|
return makeValueBoolean(hasField), nil
|
|
|
|
case *ast.Function:
|
|
return &valueFunction{
|
|
ec: makeClosure(i.stack.getCurrentEnv(a), node),
|
|
}, nil
|
|
|
|
case *ast.Apply:
|
|
// Eval target
|
|
target, err := i.evaluate(node.Target, nonTailCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
function, err := i.getFunction(target, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// environment in which we can evaluate arguments
|
|
argEnv := i.stack.getCurrentEnv(a)
|
|
arguments := callArguments{
|
|
positional: make([]*cachedThunk, len(node.Arguments.Positional)),
|
|
named: make([]namedCallArgument, len(node.Arguments.Named)),
|
|
tailstrict: node.TailStrict,
|
|
}
|
|
for i, arg := range node.Arguments.Positional {
|
|
arguments.positional[i] = &cachedThunk{env: &argEnv, body: arg}
|
|
}
|
|
|
|
for i, arg := range node.Arguments.Named {
|
|
arguments.named[i] = namedCallArgument{name: arg.Name, pv: &cachedThunk{env: &argEnv, body: arg.Arg}}
|
|
}
|
|
return i.evaluateTailCall(function, arguments, tc, trace)
|
|
|
|
case *astMakeArrayElement:
|
|
arguments := callArguments{
|
|
positional: []*cachedThunk{
|
|
&cachedThunk{
|
|
content: intToValue(node.index),
|
|
},
|
|
},
|
|
}
|
|
return i.evaluateTailCall(node.function, arguments, tc, trace)
|
|
|
|
default:
|
|
panic(fmt.Sprintf("Executing this AST type not implemented: %v", reflect.TypeOf(a)))
|
|
}
|
|
}
|
|
|
|
// unparseString Wraps in "" and escapes stuff to make the string JSON-compliant and human-readable.
|
|
func unparseString(v string) string {
|
|
var buf bytes.Buffer
|
|
buf.WriteString("\"")
|
|
for _, c := range v {
|
|
switch c {
|
|
case '"':
|
|
buf.WriteString("\\\"")
|
|
case '\\':
|
|
buf.WriteString("\\\\")
|
|
case '\b':
|
|
buf.WriteString("\\b")
|
|
case '\f':
|
|
buf.WriteString("\\f")
|
|
case '\n':
|
|
buf.WriteString("\\n")
|
|
case '\r':
|
|
buf.WriteString("\\r")
|
|
case '\t':
|
|
buf.WriteString("\\t")
|
|
case 0:
|
|
buf.WriteString("\\u0000")
|
|
default:
|
|
if c < 0x20 || (c >= 0x7f && c <= 0x9f) {
|
|
buf.WriteString(fmt.Sprintf("\\u%04x", int(c)))
|
|
} else {
|
|
buf.WriteRune(c)
|
|
}
|
|
}
|
|
}
|
|
buf.WriteString("\"")
|
|
return buf.String()
|
|
}
|
|
|
|
func unparseNumber(v float64) string {
|
|
if v == math.Floor(v) {
|
|
return fmt.Sprintf("%.0f", v)
|
|
}
|
|
|
|
// See "What Every Computer Scientist Should Know About Floating-Point Arithmetic"
|
|
// Theorem 15
|
|
// http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
|
return fmt.Sprintf("%.17g", v)
|
|
}
|
|
|
|
// manifestJSON converts to standard JSON representation as in "encoding/json" package
|
|
func (i *interpreter) manifestJSON(trace TraceElement, v value) (interface{}, error) {
|
|
switch v := v.(type) {
|
|
|
|
case *valueBoolean:
|
|
return v.value, nil
|
|
|
|
case *valueFunction:
|
|
return nil, makeRuntimeError("couldn't manifest function in JSON output.", i.getCurrentStackTrace(trace))
|
|
|
|
case *valueNumber:
|
|
return v.value, nil
|
|
|
|
case *valueString:
|
|
return v.getString(), nil
|
|
|
|
case *valueNull:
|
|
return nil, nil
|
|
|
|
case *valueArray:
|
|
result := make([]interface{}, 0, len(v.elements))
|
|
for _, th := range v.elements {
|
|
elVal, err := i.evaluatePV(th, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elem, err := i.manifestJSON(trace, elVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, elem)
|
|
}
|
|
return result, nil
|
|
|
|
case *valueObject:
|
|
fieldNames := objectFields(v, withoutHidden)
|
|
sort.Strings(fieldNames)
|
|
|
|
err := checkAssertions(i, trace, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(map[string]interface{})
|
|
|
|
for _, fieldName := range fieldNames {
|
|
fieldVal, err := v.index(i, trace, fieldName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
field, err := i.manifestJSON(trace, fieldVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[fieldName] = field
|
|
}
|
|
|
|
return result, nil
|
|
|
|
default:
|
|
return nil, makeRuntimeError(
|
|
fmt.Sprintf("manifesting this value not implemented yet: %s", reflect.TypeOf(v)),
|
|
i.getCurrentStackTrace(trace),
|
|
)
|
|
|
|
}
|
|
}
|
|
|
|
func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buffer) {
|
|
switch v := v.(type) {
|
|
case nil:
|
|
buf.WriteString("null")
|
|
|
|
case []interface{}:
|
|
if len(v) == 0 {
|
|
buf.WriteString("[ ]")
|
|
} else {
|
|
var prefix string
|
|
var indent2 string
|
|
if multiline {
|
|
prefix = "[\n"
|
|
indent2 = indent + " "
|
|
} else {
|
|
prefix = "["
|
|
indent2 = indent
|
|
}
|
|
for _, elem := range v {
|
|
buf.WriteString(prefix)
|
|
buf.WriteString(indent2)
|
|
serializeJSON(elem, multiline, indent2, buf)
|
|
if multiline {
|
|
prefix = ",\n"
|
|
} else {
|
|
prefix = ", "
|
|
}
|
|
}
|
|
if multiline {
|
|
buf.WriteString("\n")
|
|
}
|
|
buf.WriteString(indent)
|
|
buf.WriteString("]")
|
|
}
|
|
|
|
case bool:
|
|
if v {
|
|
buf.WriteString("true")
|
|
} else {
|
|
buf.WriteString("false")
|
|
}
|
|
|
|
case float64:
|
|
buf.WriteString(unparseNumber(v))
|
|
|
|
case map[string]interface{}:
|
|
fieldNames := make([]string, 0, len(v))
|
|
for name := range v {
|
|
fieldNames = append(fieldNames, name)
|
|
}
|
|
sort.Strings(fieldNames)
|
|
|
|
if len(fieldNames) == 0 {
|
|
buf.WriteString("{ }")
|
|
} else {
|
|
var prefix string
|
|
var indent2 string
|
|
if multiline {
|
|
prefix = "{\n"
|
|
indent2 = indent + " "
|
|
} else {
|
|
prefix = "{"
|
|
indent2 = indent
|
|
}
|
|
for _, fieldName := range fieldNames {
|
|
fieldVal := v[fieldName]
|
|
|
|
buf.WriteString(prefix)
|
|
buf.WriteString(indent2)
|
|
|
|
buf.WriteString(unparseString(fieldName))
|
|
buf.WriteString(": ")
|
|
|
|
serializeJSON(fieldVal, multiline, indent2, buf)
|
|
|
|
if multiline {
|
|
prefix = ",\n"
|
|
} else {
|
|
prefix = ", "
|
|
}
|
|
}
|
|
|
|
if multiline {
|
|
buf.WriteString("\n")
|
|
}
|
|
buf.WriteString(indent)
|
|
buf.WriteString("}")
|
|
}
|
|
|
|
case string:
|
|
buf.WriteString(unparseString(v))
|
|
|
|
default:
|
|
panic(fmt.Sprintf("Unsupported value for serialization %#+v", v))
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) manifestAndSerializeJSON(
|
|
buf *bytes.Buffer, trace TraceElement, v value, multiline bool, indent string) error {
|
|
manifested, err := i.manifestJSON(trace, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serializeJSON(manifested, multiline, indent, buf)
|
|
return nil
|
|
}
|
|
|
|
// manifestString expects the value to be a string and returns it.
|
|
func (i *interpreter) manifestString(buf *bytes.Buffer, trace TraceElement, v value) error {
|
|
switch v := v.(type) {
|
|
case *valueString:
|
|
buf.WriteString(v.getString())
|
|
return nil
|
|
default:
|
|
return makeRuntimeError(fmt.Sprintf("expected string result, got: %s", v.getType().name), i.getCurrentStackTrace(trace))
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) manifestAndSerializeMulti(trace TraceElement, v value, stringOutputMode bool) (r map[string]string, err error) {
|
|
r = make(map[string]string)
|
|
json, err := i.manifestJSON(trace, v)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
switch json := json.(type) {
|
|
case map[string]interface{}:
|
|
for filename, fileJSON := range json {
|
|
if stringOutputMode {
|
|
switch val := fileJSON.(type) {
|
|
case string:
|
|
r[filename] = val
|
|
default:
|
|
msg := fmt.Sprintf("multi mode: top-level object's key %s has a value of type %T, "+
|
|
"should be a string", filename, val)
|
|
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
|
|
}
|
|
} else {
|
|
var buf bytes.Buffer
|
|
serializeJSON(fileJSON, true, "", &buf)
|
|
buf.WriteString("\n")
|
|
r[filename] = buf.String()
|
|
}
|
|
}
|
|
default:
|
|
msg := fmt.Sprintf("multi mode: top-level object was a %s, "+
|
|
"should be an object whose keys are filenames and values hold "+
|
|
"the JSON for that file.", v.getType().name)
|
|
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (i *interpreter) manifestAndSerializeYAMLStream(trace TraceElement, v value) (r []string, err error) {
|
|
r = make([]string, 0)
|
|
json, err := i.manifestJSON(trace, v)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
switch json := json.(type) {
|
|
case []interface{}:
|
|
for _, doc := range json {
|
|
var buf bytes.Buffer
|
|
serializeJSON(doc, true, "", &buf)
|
|
buf.WriteString("\n")
|
|
r = append(r, buf.String())
|
|
}
|
|
default:
|
|
msg := fmt.Sprintf("stream mode: top-level object was a %s, "+
|
|
"should be an array whose elements hold "+
|
|
"the JSON for each document in the stream.", v.getType().name)
|
|
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
|
|
}
|
|
return
|
|
}
|
|
|
|
func jsonToValue(i *interpreter, trace TraceElement, v interface{}) (value, error) {
|
|
switch v := v.(type) {
|
|
case nil:
|
|
return &nullValue, nil
|
|
|
|
case []interface{}:
|
|
elems := make([]*cachedThunk, len(v))
|
|
for counter, elem := range v {
|
|
val, err := jsonToValue(i, trace, elem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elems[counter] = readyThunk(val)
|
|
}
|
|
return makeValueArray(elems), nil
|
|
|
|
case bool:
|
|
return makeValueBoolean(v), nil
|
|
case float64:
|
|
return makeValueNumber(v), nil
|
|
|
|
case map[string]interface{}:
|
|
fieldMap := map[string]value{}
|
|
for name, f := range v {
|
|
val, err := jsonToValue(i, trace, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fieldMap[name] = val
|
|
}
|
|
return buildObject(ast.ObjectFieldInherit, fieldMap), nil
|
|
|
|
case string:
|
|
return makeValueString(v), nil
|
|
|
|
default:
|
|
return nil, i.Error(fmt.Sprintf("Not a json type: %#+v", v), trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) EvalInCleanEnv(fromWhere TraceElement, env *environment, ast ast.Node, trimmable bool) (value, error) {
|
|
err := i.newCall(fromWhere, *env, trimmable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stackSize := len(i.stack.stack)
|
|
|
|
val, err := i.evaluate(ast, tailCall)
|
|
|
|
i.stack.popIfExists(stackSize)
|
|
|
|
return val, err
|
|
}
|
|
|
|
func (i *interpreter) evaluatePV(ph potentialValue, trace TraceElement) (value, error) {
|
|
return ph.getValue(i, trace)
|
|
}
|
|
|
|
func (i *interpreter) evaluateTailCall(function *valueFunction, args callArguments, tc tailCallStatus, trace TraceElement) (value, error) {
|
|
if tc == tailCall {
|
|
i.stack.tailCallTrimStack()
|
|
}
|
|
return function.call(i, trace, args)
|
|
}
|
|
|
|
func (i *interpreter) Error(s string, trace TraceElement) error {
|
|
err := makeRuntimeError(s, i.getCurrentStackTrace(trace))
|
|
return err
|
|
}
|
|
|
|
func (i *interpreter) typeErrorSpecific(bad value, good value, trace TraceElement) error {
|
|
return i.Error(
|
|
fmt.Sprintf("Unexpected type %v, expected %v", bad.getType().name, good.getType().name),
|
|
trace,
|
|
)
|
|
}
|
|
|
|
func (i *interpreter) typeErrorGeneral(bad value, trace TraceElement) error {
|
|
return i.Error(
|
|
fmt.Sprintf("Unexpected type %v", bad.getType().name),
|
|
trace,
|
|
)
|
|
}
|
|
|
|
func (i *interpreter) getNumber(val value, trace TraceElement) (*valueNumber, error) {
|
|
switch v := val.(type) {
|
|
case *valueNumber:
|
|
return v, nil
|
|
default:
|
|
return nil, i.typeErrorSpecific(val, &valueNumber{}, trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) evaluateNumber(pv potentialValue, trace TraceElement) (*valueNumber, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getNumber(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getInt(val value, trace TraceElement) (int, error) {
|
|
num, err := i.getNumber(val, trace)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
// We conservatively convert ot int32, so that it can be machine-sized int
|
|
// on any machine. And it's used only for indexing anyway.
|
|
intNum := int(int32(num.value))
|
|
if float64(intNum) != num.value {
|
|
return 0, i.Error(fmt.Sprintf("Expected an integer, but got %v", num.value), trace)
|
|
}
|
|
return intNum, nil
|
|
}
|
|
|
|
func (i *interpreter) evaluateInt(pv potentialValue, trace TraceElement) (int, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return i.getInt(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getInt64(val value, trace TraceElement) (int64, error) {
|
|
num, err := i.getNumber(val, trace)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
intNum := int64(num.value)
|
|
if float64(intNum) != num.value {
|
|
return 0, i.Error(fmt.Sprintf("Expected an integer, but got %v", num.value), trace)
|
|
}
|
|
return intNum, nil
|
|
}
|
|
|
|
func (i *interpreter) evaluateInt64(pv potentialValue, trace TraceElement) (int64, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return i.getInt64(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getString(val value, trace TraceElement) (*valueString, error) {
|
|
switch v := val.(type) {
|
|
case *valueString:
|
|
return v, nil
|
|
default:
|
|
return nil, i.typeErrorSpecific(val, &valueString{}, trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) evaluateString(pv potentialValue, trace TraceElement) (*valueString, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getString(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getBoolean(val value, trace TraceElement) (*valueBoolean, error) {
|
|
switch v := val.(type) {
|
|
case *valueBoolean:
|
|
return v, nil
|
|
default:
|
|
return nil, i.typeErrorSpecific(val, &valueBoolean{}, trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) evaluateBoolean(pv potentialValue, trace TraceElement) (*valueBoolean, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getBoolean(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getArray(val value, trace TraceElement) (*valueArray, error) {
|
|
switch v := val.(type) {
|
|
case *valueArray:
|
|
return v, nil
|
|
default:
|
|
return nil, i.typeErrorSpecific(val, &valueArray{}, trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) evaluateArray(pv potentialValue, trace TraceElement) (*valueArray, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getArray(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getFunction(val value, trace TraceElement) (*valueFunction, error) {
|
|
switch v := val.(type) {
|
|
case *valueFunction:
|
|
return v, nil
|
|
default:
|
|
return nil, i.typeErrorSpecific(val, &valueFunction{}, trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) evaluateFunction(pv potentialValue, trace TraceElement) (*valueFunction, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getFunction(v, trace)
|
|
}
|
|
|
|
func (i *interpreter) getObject(val value, trace TraceElement) (*valueObject, error) {
|
|
switch v := val.(type) {
|
|
case *valueObject:
|
|
return v, nil
|
|
default:
|
|
return nil, i.typeErrorSpecific(val, &valueObject{}, trace)
|
|
}
|
|
}
|
|
|
|
func (i *interpreter) evaluateObject(pv potentialValue, trace TraceElement) (*valueObject, error) {
|
|
v, err := i.evaluatePV(pv, trace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.getObject(v, trace)
|
|
}
|
|
|
|
func buildStdObject(i *interpreter) (*valueObject, error) {
|
|
objVal, err := evaluateStd(i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
obj := objVal.(*valueObject).uncached.(*simpleObject)
|
|
builtinFields := map[string]unboundField{}
|
|
for key, ec := range funcBuiltins {
|
|
function := valueFunction{ec: ec} // TODO(sbarzowski) better way to build function value
|
|
builtinFields[key] = &readyValue{&function}
|
|
}
|
|
|
|
for name, value := range builtinFields {
|
|
obj.fields[name] = simpleObjectField{ast.ObjectFieldHidden, value}
|
|
}
|
|
return objVal.(*valueObject), nil
|
|
}
|
|
|
|
func evaluateStd(i *interpreter) (value, error) {
|
|
beforeStdEnv := makeEnvironment(
|
|
bindingFrame{},
|
|
makeUnboundSelfBinding(),
|
|
)
|
|
evalLoc := ast.MakeLocationRangeMessage("During evaluation of std")
|
|
evalTrace := TraceElement{loc: &evalLoc}
|
|
node := ast.StdAst
|
|
return i.EvalInCleanEnv(evalTrace, &beforeStdEnv, node, false)
|
|
}
|
|
|
|
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[string]*cachedThunk {
|
|
result := make(map[string]*cachedThunk)
|
|
for name, content := range ext {
|
|
if content.isCode {
|
|
result[name] = codeToPV(i, "<"+kind+":"+name+">", content.value)
|
|
} else {
|
|
result[name] = readyThunk(makeValueString(content.value))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func buildObject(hide ast.ObjectFieldHide, fields map[string]value) *valueObject {
|
|
fieldMap := simpleObjectFieldMap{}
|
|
for name, v := range fields {
|
|
fieldMap[name] = simpleObjectField{hide, &readyValue{v}}
|
|
}
|
|
return makeValueSimpleObject(bindingFrame{}, fieldMap, nil, nil)
|
|
}
|
|
|
|
func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, importer Importer) (*interpreter, error) {
|
|
i := interpreter{
|
|
stack: makeCallStack(maxStack),
|
|
importCache: MakeImportCache(importer),
|
|
nativeFuncs: nativeFuncs,
|
|
}
|
|
|
|
stdObj, err := buildStdObject(&i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i.baseStd = stdObj
|
|
|
|
i.extVars = prepareExtVars(&i, ext, "extvar")
|
|
|
|
return &i, nil
|
|
}
|
|
|
|
func makeInitialEnv(filename string, baseStd *valueObject) environment {
|
|
fileSpecific := buildObject(ast.ObjectFieldHidden, map[string]value{
|
|
"thisFile": makeValueString(filename),
|
|
})
|
|
return makeEnvironment(
|
|
bindingFrame{
|
|
"std": readyThunk(makeValueExtendedObject(baseStd, fileSpecific)),
|
|
},
|
|
makeUnboundSelfBinding(),
|
|
)
|
|
}
|
|
|
|
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, TraceElement, error) {
|
|
evalLoc := ast.MakeLocationRangeMessage("During evaluation")
|
|
evalTrace := TraceElement{
|
|
loc: &evalLoc,
|
|
}
|
|
env := makeInitialEnv(node.Loc().FileName, i.baseStd)
|
|
result, err := i.EvalInCleanEnv(evalTrace, &env, node, false)
|
|
if err != nil {
|
|
return nil, TraceElement{}, err
|
|
}
|
|
// If it's not a function, ignore TLA
|
|
if f, ok := result.(*valueFunction); ok {
|
|
toplevelArgMap := prepareExtVars(i, tla, "top-level-arg")
|
|
args := callArguments{}
|
|
for argName, pv := range toplevelArgMap {
|
|
args.named = append(args.named, namedCallArgument{name: ast.Identifier(argName), pv: pv})
|
|
}
|
|
funcLoc := ast.MakeLocationRangeMessage("Top-level function")
|
|
funcTrace := TraceElement{
|
|
loc: &funcLoc,
|
|
}
|
|
result, err = f.call(i, funcTrace, args)
|
|
if err != nil {
|
|
return nil, TraceElement{}, err
|
|
}
|
|
}
|
|
manifestationLoc := ast.MakeLocationRangeMessage("During manifestation")
|
|
manifestationTrace := TraceElement{
|
|
loc: &manifestationLoc,
|
|
}
|
|
return result, manifestationTrace, nil
|
|
}
|
|
|
|
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
|
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
|
maxStack int, importer Importer, stringOutputMode bool) (string, error) {
|
|
|
|
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
result, manifestationTrace, err := evaluateAux(i, node, tla)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if stringOutputMode {
|
|
err = i.manifestString(&buf, manifestationTrace, result)
|
|
} else {
|
|
err = i.manifestAndSerializeJSON(&buf, manifestationTrace, result, true, "")
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
buf.WriteString("\n")
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
|
func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
|
maxStack int, importer Importer, stringOutputMode bool) (map[string]string, error) {
|
|
|
|
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, manifestationTrace, err := evaluateAux(i, node, tla)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return i.manifestAndSerializeMulti(manifestationTrace, result, stringOutputMode)
|
|
}
|
|
|
|
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
|
func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
|
maxStack int, importer Importer) ([]string, error) {
|
|
|
|
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, manifestationTrace, err := evaluateAux(i, node, tla)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return i.manifestAndSerializeYAMLStream(manifestationTrace, result)
|
|
}
|