Internal refactor to improve performance (#225)

* Internal refactor to improve performance
This commit is contained in:
Dave Cunningham 2018-06-01 10:52:20 -04:00 committed by GitHub
parent 530cf7b07c
commit 2973e24152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 774 additions and 910 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,227 +0,0 @@
/*
Copyright 2017 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 (
"fmt"
"github.com/google/go-jsonnet/ast"
)
// evaluator is a convenience wrapper for interpreter
// Most importantly it keeps the context for traces and handles details
// of error handling.
type evaluator struct {
i *interpreter
trace *TraceElement
}
func makeEvaluator(i *interpreter, trace *TraceElement) *evaluator {
return &evaluator{i: i, trace: trace}
}
func (e *evaluator) inNewContext(trace *TraceElement) *evaluator {
return makeEvaluator(e.i, trace)
}
func (e *evaluator) evaluate(ph potentialValue) (value, error) {
return ph.getValue(e.i, e.trace)
}
func (e *evaluator) evaluateTailCall(ph potentialValue, tc tailCallStatus) (value, error) {
if tc == tailCall {
e.i.stack.tailCallTrimStack()
}
return ph.getValue(e.i, e.trace)
}
func (e *evaluator) Error(s string) error {
err := makeRuntimeError(s, e.i.getCurrentStackTrace(e.trace))
return err
}
func (e *evaluator) typeErrorSpecific(bad value, good value) error {
return e.Error(
fmt.Sprintf("Unexpected type %v, expected %v", bad.getType().name, good.getType().name),
)
}
func (e *evaluator) typeErrorGeneral(bad value) error {
return e.Error(
fmt.Sprintf("Unexpected type %v", bad.getType().name),
)
}
func (e *evaluator) getNumber(val value) (*valueNumber, error) {
switch v := val.(type) {
case *valueNumber:
return v, nil
default:
return nil, e.typeErrorSpecific(val, &valueNumber{})
}
}
func (e *evaluator) evaluateNumber(pv potentialValue) (*valueNumber, error) {
v, err := e.evaluate(pv)
if err != nil {
return nil, err
}
return e.getNumber(v)
}
func (e *evaluator) getInt(val value) (int, error) {
num, err := e.getNumber(val)
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, e.Error(fmt.Sprintf("Expected an integer, but got %v", num.value))
}
return intNum, nil
}
func (e *evaluator) evaluateInt(pv potentialValue) (int, error) {
v, err := e.evaluate(pv)
if err != nil {
return 0, err
}
return e.getInt(v)
}
func (e *evaluator) getInt64(val value) (int64, error) {
num, err := e.getNumber(val)
if err != nil {
return 0, err
}
intNum := int64(num.value)
if float64(intNum) != num.value {
return 0, e.Error(fmt.Sprintf("Expected an integer, but got %v", num.value))
}
return intNum, nil
}
func (e *evaluator) evaluateInt64(pv potentialValue) (int64, error) {
v, err := e.evaluate(pv)
if err != nil {
return 0, err
}
return e.getInt64(v)
}
func (e *evaluator) getString(val value) (*valueString, error) {
switch v := val.(type) {
case *valueString:
return v, nil
default:
return nil, e.typeErrorSpecific(val, &valueString{})
}
}
func (e *evaluator) evaluateString(pv potentialValue) (*valueString, error) {
v, err := e.evaluate(pv)
if err != nil {
return nil, err
}
return e.getString(v)
}
func (e *evaluator) getBoolean(val value) (*valueBoolean, error) {
switch v := val.(type) {
case *valueBoolean:
return v, nil
default:
return nil, e.typeErrorSpecific(val, &valueBoolean{})
}
}
func (e *evaluator) evaluateBoolean(pv potentialValue) (*valueBoolean, error) {
v, err := e.evaluate(pv)
if err != nil {
return nil, err
}
return e.getBoolean(v)
}
func (e *evaluator) getArray(val value) (*valueArray, error) {
switch v := val.(type) {
case *valueArray:
return v, nil
default:
return nil, e.typeErrorSpecific(val, &valueArray{})
}
}
func (e *evaluator) evaluateArray(pv potentialValue) (*valueArray, error) {
v, err := e.evaluate(pv)
if err != nil {
return nil, err
}
return e.getArray(v)
}
func (e *evaluator) getFunction(val value) (*valueFunction, error) {
switch v := val.(type) {
case *valueFunction:
return v, nil
default:
return nil, e.typeErrorSpecific(val, &valueFunction{})
}
}
func (e *evaluator) evaluateFunction(pv potentialValue) (*valueFunction, error) {
v, err := e.evaluate(pv)
if err != nil {
return nil, err
}
return e.getFunction(v)
}
func (e *evaluator) getObject(val value) (valueObject, error) {
switch v := val.(type) {
case valueObject:
return v, nil
default:
return nil, e.typeErrorSpecific(val, &valueSimpleObject{})
}
}
func (e *evaluator) evaluateObject(pv potentialValue) (valueObject, error) {
v, err := e.evaluate(pv)
if err != nil {
return nil, err
}
return e.getObject(v)
}
func (e *evaluator) evalInCurrentContext(a ast.Node, tc tailCallStatus) (value, error) {
return e.i.evaluate(a, tc)
}
func (e *evaluator) evalInCleanEnv(env *environment, ast ast.Node, trimmable bool) (value, error) {
return e.i.EvalInCleanEnv(e.trace, env, ast, trimmable)
}
func (e *evaluator) lookUpVar(ident ast.Identifier) potentialValue {
th := e.i.stack.lookUpVar(ident)
if th == nil {
panic(fmt.Sprintf("RUNTIME: Unknown variable: %v (we should have caught this statically)", ident))
}
return th
}

View File

@ -98,15 +98,15 @@ func (cache *ImportCache) importData(importedFrom, importedPath string) (content
}
// ImportString imports a string, caches it and then returns it.
func (cache *ImportCache) ImportString(importedFrom, importedPath string, e *evaluator) (*valueString, error) {
func (cache *ImportCache) ImportString(importedFrom, importedPath string, i *interpreter, trace TraceElement) (*valueString, error) {
data, _, err := cache.importData(importedFrom, importedPath)
if err != nil {
return nil, e.Error(err.Error())
return nil, i.Error(err.Error(), trace)
}
return makeValueString(data.String()), nil
}
func codeToPV(e *evaluator, filename string, code string) potentialValue {
func codeToPV(i *interpreter, filename string, code string) *cachedThunk {
node, err := snippetToAST(filename, code)
if err != nil {
// TODO(sbarzowski) we should wrap (static) error here
@ -114,26 +114,31 @@ func codeToPV(e *evaluator, filename string, code string) potentialValue {
// actually depends on what happens in Runtime (whether import gets
// evaluated).
// The same thinking applies to external variables.
return makeErrorThunk(err)
return &cachedThunk{err: err}
}
env := makeInitialEnv(filename, i.baseStd)
return &cachedThunk{
env: &env,
body: node,
content: nil,
}
return makeThunk(makeInitialEnv(filename, e.i.baseStd), node)
}
// ImportCode imports code from a path.
func (cache *ImportCache) ImportCode(importedFrom, importedPath string, e *evaluator) (value, error) {
func (cache *ImportCache) ImportCode(importedFrom, importedPath string, i *interpreter, trace TraceElement) (value, error) {
contents, foundAt, err := cache.importData(importedFrom, importedPath)
if err != nil {
return nil, e.Error(err.Error())
return nil, i.Error(err.Error(), trace)
}
var pv potentialValue
if cachedPV, isCached := cache.codeCache[foundAt]; !isCached {
// File hasn't been parsed and analyzed before, update the cache record.
pv = codeToPV(e, foundAt, contents.String())
pv = codeToPV(i, foundAt, contents.String())
cache.codeCache[foundAt] = pv
} else {
pv = cachedPV
}
return e.evaluate(pv)
return i.evaluatePV(pv, trace)
}
// Concrete importers

View File

@ -29,7 +29,7 @@ import (
// 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 {
sb selfBinding
selfBinding selfBinding
// Bindings introduced in this frame. The way previous bindings are treated
// depends on the type of a frame.
@ -43,22 +43,18 @@ type environment struct {
func makeEnvironment(upValues bindingFrame, sb selfBinding) environment {
return environment{
upValues: upValues,
sb: sb,
selfBinding: sb,
}
}
func callFrameToTraceFrame(frame *callFrame) TraceFrame {
return traceElementToTraceFrame(frame.trace)
}
func (i *interpreter) getCurrentStackTrace(additional *TraceElement) []TraceFrame {
func (i *interpreter) getCurrentStackTrace(additional TraceElement) []TraceFrame {
var result []TraceFrame
for _, f := range i.stack.stack {
if f.isCall {
result = append(result, callFrameToTraceFrame(f))
result = append(result, traceElementToTraceFrame(f.trace))
}
}
if additional != nil {
if additional.loc != nil {
result = append(result, traceElementToTraceFrame(additional))
}
return result
@ -71,7 +67,7 @@ type callFrame struct {
isCall bool
// Tracing information about the place where it was called from.
trace *TraceElement
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
@ -84,7 +80,7 @@ type callFrame struct {
func dumpCallFrame(c *callFrame) string {
var loc ast.LocationRange
if c.trace == nil || c.trace.loc == nil {
if c.trace.loc == nil {
loc = ast.MakeLocationRangeMessage("?")
} else {
loc = *c.trace.loc
@ -151,11 +147,7 @@ const (
tailCall
)
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))
}
func (s *callStack) newCall(trace TraceElement, env environment, trimmable bool) {
s.stack = append(s.stack, &callFrame{
isCall: true,
trace: trace,
@ -163,11 +155,9 @@ func (i *interpreter) newCall(trace *TraceElement, env environment, trimmable bo
trimmable: trimmable,
})
s.calls++
return nil
}
func (i *interpreter) newLocal(vars bindingFrame) {
s := &i.stack
func (s *callStack) newLocal(vars bindingFrame) {
s.stack = append(s.stack, &callFrame{
env: makeEnvironment(vars, selfBinding{}),
})
@ -177,17 +167,17 @@ func (i *interpreter) newLocal(vars bindingFrame) {
func (s *callStack) getSelfBinding() selfBinding {
for i := len(s.stack) - 1; i >= 0; i-- {
if s.stack[i].isCall {
return s.stack[i].env.sb
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) potentialValue {
func (s *callStack) lookUpVar(id ast.Identifier) *cachedThunk {
for i := len(s.stack) - 1; i >= 0; i-- {
bind := s.stack[i].env.upValues[id]
if bind != nil {
bind, present := s.stack[i].env.upValues[id]
if present {
return bind
}
if s.stack[i].isCall {
@ -198,6 +188,30 @@ func (s *callStack) lookUpVar(id ast.Identifier) potentialValue {
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(),
)
}
// 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,
@ -213,7 +227,7 @@ type interpreter struct {
stack callStack
// External variables
extVars map[string]potentialValue
extVars map[string]*cachedThunk
// Native functions
nativeFuncs map[string]*NativeFunction
@ -225,18 +239,7 @@ type interpreter struct {
importCache *ImportCache
}
// Build a binding frame containing specified variables.
func (i *interpreter) capture(freeVars ast.Identifiers) bindingFrame {
env := make(bindingFrame)
for _, fv := range freeVars {
env[fv] = i.stack.lookUpVar(fv)
if env[fv] == nil {
panic(fmt.Sprintf("Variable %v vanished", fv))
}
}
return env
}
// Map union, b takes precedence when keys collide.
func addBindings(a, b bindingFrame) bindingFrame {
result := make(bindingFrame)
@ -251,82 +254,117 @@ func addBindings(a, b bindingFrame) bindingFrame {
return result
}
func (i *interpreter) getCurrentEnv(ast ast.Node) environment {
return makeEnvironment(
i.capture(ast.FreeVariables()),
i.stack.getSelfBinding(),
)
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) {
e := &evaluator{
trace: &TraceElement{
trace := TraceElement{
loc: a.Loc(),
context: a.Context(),
},
i: i,
}
switch node := a.(type) {
case *ast.Array:
sb := i.stack.getSelfBinding()
var elements []potentialValue
var elements []*cachedThunk
for _, el := range node.Elements {
env := makeEnvironment(i.capture(el.FreeVariables()), sb)
elThunk := makeThunk(env, el)
elements = append(elements, elThunk)
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:
// Some binary operators are lazy, so thunks are needed in general
env := i.getCurrentEnv(node)
// TODO(sbarzowski) make sure it displays nicely in stack trace (thunk names etc.)
// TODO(sbarzowski) it may make sense not to show a line in stack trace for operators
// at all in many cases. 1 + 2 + 3 + 4 + error "x" will show 5 lines
// of stack trace now, and it's not that nice.
left := makeThunk(env, node.Left)
right := makeThunk(env, node.Right)
builtin := bopBuiltins[node.Op]
result, err := builtin.function(e, left, right)
if node.Op == ast.BopAnd {
// Special case for shortcut semantics.
xv, err := i.evaluate(node.Left, tc)
if err != nil {
return nil, err
}
return result, nil
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, tc)
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, tc)
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:
env := i.getCurrentEnv(node)
arg := makeThunk(env, node.Expr)
value, err := i.evaluate(node.Expr, tc)
if err != nil {
return nil, err
}
builtin := uopBuiltins[node.Op]
result, err := builtin.function(e, arg)
result, err := builtin.function(i, trace, value)
if err != nil {
return nil, err
}
return result, nil
case *ast.Conditional:
cond, err := e.evalInCurrentContext(node.Cond, nonTailCall)
cond, err := i.evaluate(node.Cond, nonTailCall)
if err != nil {
return nil, err
}
condBool, err := e.getBoolean(cond)
condBool, err := i.getBoolean(cond, trace)
if err != nil {
return nil, err
}
if condBool.value {
return e.evalInCurrentContext(node.BranchTrue, tc)
return i.evaluate(node.BranchTrue, tc)
}
return e.evalInCurrentContext(node.BranchFalse, 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 := e.evalInCurrentContext(field.Name, nonTailCall)
fieldNameValue, err := i.evaluate(field.Name, nonTailCall)
if err != nil {
return nil, err
}
@ -338,11 +376,11 @@ func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) {
// Omitted field.
continue
default:
return nil, e.Error(fmt.Sprintf("Field name must be string, got %v", fieldNameValue.getType().name))
return nil, i.Error(fmt.Sprintf("Field name must be string, got %v", fieldNameValue.getType().name), trace)
}
if _, ok := fields[fieldName]; ok {
return nil, e.Error(duplicateFieldNameErrMsg(fieldName))
return nil, i.Error(duplicateFieldNameErrMsg(fieldName), trace)
}
var f unboundField = &codeUnboundField{field.Body}
if field.PlusSuper {
@ -354,69 +392,69 @@ func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) {
for _, assert := range node.Asserts {
asserts = append(asserts, &codeUnboundField{assert})
}
upValues := i.capture(node.FreeVariables())
upValues := i.stack.capture(node.FreeVariables())
return makeValueSimpleObject(upValues, fields, asserts), nil
case *ast.Error:
msgVal, err := e.evalInCurrentContext(node.Expr, nonTailCall)
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(e, &readyValue{msgVal})
msgVal, err = builtinToString(i, trace, msgVal)
if err != nil {
return nil, err
}
}
msg, err := e.getString(msgVal)
msg, err := i.getString(msgVal, trace)
if err != nil {
return nil, err
}
return nil, e.Error(msg.getString())
return nil, i.Error(msg.getString(), trace)
case *ast.Index:
targetValue, err := e.evalInCurrentContext(node.Target, nonTailCall)
targetValue, err := i.evaluate(node.Target, nonTailCall)
if err != nil {
return nil, err
}
index, err := e.evalInCurrentContext(node.Index, nonTailCall)
index, err := i.evaluate(node.Index, nonTailCall)
if err != nil {
return nil, err
}
switch target := targetValue.(type) {
case valueObject:
indexString, err := e.getString(index)
indexString, err := i.getString(index, trace)
if err != nil {
return nil, err
}
return target.index(e, indexString.getString())
return target.index(i, trace, indexString.getString())
case *valueArray:
indexInt, err := e.getNumber(index)
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(e, int(indexInt.value), tc)
return target.index(i, trace, int(indexInt.value))
case *valueString:
indexInt, err := e.getNumber(index)
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(e, int(indexInt.value))
return target.index(i, trace, int(indexInt.value))
}
return nil, e.Error(fmt.Sprintf("Value non indexable: %v", reflect.TypeOf(targetValue)))
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, e)
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, e)
return i.importCache.ImportString(codePath, node.File.Value, i, trace)
case *ast.LiteralBoolean:
return makeValueBoolean(node.Value), nil
@ -432,19 +470,19 @@ func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) {
case *ast.Local:
vars := make(bindingFrame)
bindEnv := i.getCurrentEnv(a)
bindEnv := i.stack.getCurrentEnv(a)
for _, bind := range node.Binds {
th := makeThunk(bindEnv, bind.Body)
th := cachedThunk{env: &bindEnv, body: bind.Body}
// recursive locals
vars[bind.Variable] = th
bindEnv.upValues[bind.Variable] = th
vars[bind.Variable] = &th
bindEnv.upValues[bind.Variable] = &th
}
i.newLocal(vars)
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 := e.evalInCurrentContext(node.Body, tc)
v, err := i.evaluate(node.Body, tc)
i.stack.popIfExists(sz)
return v, err
@ -454,65 +492,76 @@ func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) {
return sb.self, nil
case *ast.Var:
return e.evaluateTailCall(e.lookUpVar(node.Id), tc)
foo := i.stack.lookUpVarOrPanic(node.Id)
return foo.getValue(i, trace)
case *ast.SuperIndex:
index, err := e.evalInCurrentContext(node.Index, nonTailCall)
index, err := i.evaluate(node.Index, nonTailCall)
if err != nil {
return nil, err
}
indexStr, err := e.getString(index)
indexStr, err := i.getString(index, trace)
if err != nil {
return nil, err
}
return objectIndex(e, i.stack.getSelfBinding().super(), indexStr.getString())
return objectIndex(i, trace, i.stack.getSelfBinding().super(), indexStr.getString())
case *ast.InSuper:
index, err := e.evalInCurrentContext(node.Index, nonTailCall)
index, err := i.evaluate(node.Index, nonTailCall)
if err != nil {
return nil, err
}
indexStr, err := e.getString(index)
indexStr, err := i.getString(index, trace)
if err != nil {
return nil, err
}
field := tryObjectIndex(i.stack.getSelfBinding().super(), indexStr.getString(), withHidden)
return makeValueBoolean(field != nil), nil
hasField := objectHasField(i.stack.getSelfBinding().super(), indexStr.getString(), withHidden)
return makeValueBoolean(hasField), nil
case *ast.Function:
return &valueFunction{
ec: makeClosure(i.getCurrentEnv(a), node),
ec: makeClosure(i.stack.getCurrentEnv(a), node),
}, nil
case *ast.Apply:
// Eval target
target, err := e.evalInCurrentContext(node.Target, nonTailCall)
target, err := i.evaluate(node.Target, nonTailCall)
if err != nil {
return nil, err
}
function, err := e.getFunction(target)
function, err := i.getFunction(target, trace)
if err != nil {
return nil, err
}
// environment in which we can evaluate arguments
argEnv := i.getCurrentEnv(a)
argEnv := i.stack.getCurrentEnv(a)
arguments := callArguments{
positional: make([]potentialValue, len(node.Arguments.Positional)),
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] = makeThunk(argEnv, arg)
arguments.positional[i] = &cachedThunk{env: &argEnv, body: arg}
}
for i, arg := range node.Arguments.Named {
arguments.named[i] = namedCallArgument{name: arg.Name, pv: makeThunk(argEnv, arg.Arg)}
arguments.named[i] = namedCallArgument{name: arg.Name, pv: &cachedThunk{env: &argEnv, body: arg.Arg}}
}
return e.evaluateTailCall(function.call(arguments), tc)
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:
return nil, e.Error(fmt.Sprintf("Executing this AST type not implemented yet: %v", reflect.TypeOf(a)))
return nil, i.Error(fmt.Sprintf("Executing this AST type not implemented: %v", reflect.TypeOf(a)), trace)
}
}
@ -562,8 +611,7 @@ func unparseNumber(v float64) string {
}
// manifestJSON converts to standard JSON representation as in "encoding/json" package
func (i *interpreter) manifestJSON(trace *TraceElement, v value) (interface{}, error) {
e := &evaluator{i: i, trace: trace}
func (i *interpreter) manifestJSON(trace TraceElement, v value) (interface{}, error) {
switch v := v.(type) {
case *valueBoolean:
@ -584,7 +632,7 @@ func (i *interpreter) manifestJSON(trace *TraceElement, v value) (interface{}, e
case *valueArray:
result := make([]interface{}, 0, len(v.elements))
for _, th := range v.elements {
elVal, err := e.evaluate(th)
elVal, err := i.evaluatePV(th, trace)
if err != nil {
return nil, err
}
@ -600,7 +648,7 @@ func (i *interpreter) manifestJSON(trace *TraceElement, v value) (interface{}, e
fieldNames := objectFields(v, withoutHidden)
sort.Strings(fieldNames)
err := checkAssertions(e, v)
err := checkAssertions(i, trace, v)
if err != nil {
return nil, err
}
@ -608,7 +656,7 @@ func (i *interpreter) manifestJSON(trace *TraceElement, v value) (interface{}, e
result := make(map[string]interface{})
for _, fieldName := range fieldNames {
fieldVal, err := v.index(e, fieldName)
fieldVal, err := v.index(i, trace, fieldName)
if err != nil {
return nil, err
}
@ -729,7 +777,7 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff
}
func (i *interpreter) manifestAndSerializeJSON(
buf *bytes.Buffer, trace *TraceElement, v value, multiline bool, indent string) error {
buf *bytes.Buffer, trace TraceElement, v value, multiline bool, indent string) error {
manifested, err := i.manifestJSON(trace, v)
if err != nil {
return err
@ -739,7 +787,7 @@ func (i *interpreter) manifestAndSerializeJSON(
}
// manifestString expects the value to be a string and returns it.
func (i *interpreter) manifestString(buf *bytes.Buffer, trace *TraceElement, v value) error {
func (i *interpreter) manifestString(buf *bytes.Buffer, trace TraceElement, v value) error {
switch v := v.(type) {
case *valueString:
buf.WriteString(v.getString())
@ -749,7 +797,7 @@ func (i *interpreter) manifestString(buf *bytes.Buffer, trace *TraceElement, v v
}
}
func (i *interpreter) manifestAndSerializeMulti(trace *TraceElement, v value, stringOutputMode bool) (r map[string]string, err error) {
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 {
@ -783,7 +831,7 @@ func (i *interpreter) manifestAndSerializeMulti(trace *TraceElement, v value, st
return
}
func (i *interpreter) manifestAndSerializeYAMLStream(trace *TraceElement, v value) (r []string, err error) {
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 {
@ -806,19 +854,19 @@ func (i *interpreter) manifestAndSerializeYAMLStream(trace *TraceElement, v valu
return
}
func jsonToValue(e *evaluator, v interface{}) (value, error) {
func jsonToValue(i *interpreter, trace TraceElement, v interface{}) (value, error) {
switch v := v.(type) {
case nil:
return &nullValue, nil
case []interface{}:
elems := make([]potentialValue, len(v))
for i, elem := range v {
val, err := jsonToValue(e, elem)
elems := make([]*cachedThunk, len(v))
for counter, elem := range v {
val, err := jsonToValue(i, trace, elem)
if err != nil {
return nil, err
}
elems[i] = &readyValue{val}
elems[counter] = readyThunk(val)
}
return makeValueArray(elems), nil
@ -830,7 +878,7 @@ func jsonToValue(e *evaluator, v interface{}) (value, error) {
case map[string]interface{}:
fieldMap := map[string]value{}
for name, f := range v {
val, err := jsonToValue(e, f)
val, err := jsonToValue(i, trace, f)
if err != nil {
return nil, err
}
@ -842,11 +890,11 @@ func jsonToValue(e *evaluator, v interface{}) (value, error) {
return makeValueString(v), nil
default:
return nil, e.Error(fmt.Sprintf("Not a json type: %#+v", v))
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) {
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
@ -860,6 +908,180 @@ func (i *interpreter) EvalInCleanEnv(fromWhere *TraceElement, env *environment,
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, &valueSimpleObject{}, 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 {
@ -884,26 +1106,18 @@ func evaluateStd(i *interpreter) (value, error) {
makeUnboundSelfBinding(),
)
evalLoc := ast.MakeLocationRangeMessage("During evaluation of std")
evalTrace := &TraceElement{loc: &evalLoc}
evalTrace := TraceElement{loc: &evalLoc}
node := ast.StdAst
return i.EvalInCleanEnv(evalTrace, &beforeStdEnv, node, false)
}
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[string]potentialValue {
result := make(map[string]potentialValue)
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[string]*cachedThunk {
result := make(map[string]*cachedThunk)
for name, content := range ext {
if content.isCode {
varLoc := ast.MakeLocationRangeMessage("During evaluation")
varTrace := &TraceElement{
loc: &varLoc,
}
e := &evaluator{
i: i,
trace: varTrace,
}
result[name] = codeToPV(e, "<"+kind+":"+name+">", content.value)
result[name] = codeToPV(i, "<"+kind+":"+name+">", content.value)
} else {
result[name] = &readyValue{makeValueString(content.value)}
result[name] = readyThunk(makeValueString(content.value))
}
}
return result
@ -942,21 +1156,21 @@ func makeInitialEnv(filename string, baseStd valueObject) environment {
})
return makeEnvironment(
bindingFrame{
"std": &readyValue{makeValueExtendedObject(baseStd, fileSpecific)},
"std": readyThunk(makeValueExtendedObject(baseStd, fileSpecific)),
},
makeUnboundSelfBinding(),
)
}
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, *TraceElement, error) {
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, TraceElement, error) {
evalLoc := ast.MakeLocationRangeMessage("During evaluation")
evalTrace := &TraceElement{
evalTrace := TraceElement{
loc: &evalLoc,
}
env := makeInitialEnv(node.Loc().FileName, i.baseStd)
result, err := i.EvalInCleanEnv(evalTrace, &env, node, false)
if err != nil {
return nil, nil, err
return nil, TraceElement{}, err
}
// If it's not a function, ignore TLA
if f, ok := result.(*valueFunction); ok {
@ -966,16 +1180,16 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, *TraceElem
args.named = append(args.named, namedCallArgument{name: ast.Identifier(argName), pv: pv})
}
funcLoc := ast.MakeLocationRangeMessage("Top-level function")
funcTrace := &TraceElement{
funcTrace := TraceElement{
loc: &funcLoc,
}
result, err = f.call(args).getValue(i, funcTrace)
result, err = f.call(i, funcTrace, args)
if err != nil {
return nil, nil, err
return nil, TraceElement{}, err
}
}
manifestationLoc := ast.MakeLocationRangeMessage("During manifestation")
manifestationTrace := &TraceElement{
manifestationTrace := TraceElement{
loc: &manifestationLoc,
}
return result, manifestationTrace, nil

View File

@ -44,7 +44,7 @@ type TraceFrame struct {
Name string
}
func traceElementToTraceFrame(trace *TraceElement) TraceFrame {
func traceElementToTraceFrame(trace TraceElement) TraceFrame {
tf := TraceFrame{Loc: *trace.loc}
if trace.context != nil {
// TODO(sbarzowski) maybe it should never be nil

View File

@ -4,11 +4,6 @@ RUNTIME ERROR: x
1 & error "x"
-------------------------------------------------
testdata/bitwise_and4:1:1-14 $
1 & error "x"
-------------------------------------------------
During evaluation

View File

@ -4,11 +4,6 @@ RUNTIME ERROR: x
1 ^ error "x"
-------------------------------------------------
testdata/bitwise_xor7:1:1-14 $
1 ^ error "x"
-------------------------------------------------
During evaluation

View File

@ -4,11 +4,6 @@ RUNTIME ERROR: should happen
true && error "should happen"
-------------------------------------------------
testdata/lazy_operator2:1:1-30 $
true && error "should happen"
-------------------------------------------------
During evaluation

View File

@ -4,11 +4,6 @@ RUNTIME ERROR: Attempt to use super when there is no super class.
{ x: 5, assert super.x == 5 }
-------------------------------------------------
testdata/object_invariant7:1:16-28 object <anonymous>
{ x: 5, assert super.x == 5 }
-------------------------------------------------
During manifestation

5
testdata/or4.golden vendored
View File

@ -4,11 +4,6 @@ RUNTIME ERROR: xxx
false || error "xxx"
-------------------------------------------------
testdata/or4:1:1-21 $
false || error "xxx"
-------------------------------------------------
During evaluation

View File

@ -15,12 +15,12 @@ RUNTIME ERROR: Not enough values to format, got 1
std.toString(val)
-------------------------------------------------
<std>:558:9-26 builtin function <toString>
<std>:558:9-26 function <format_code>
std.toString(val)
-------------------------------------------------
... (skipped 14 frames)
... (skipped 10 frames)
-------------------------------------------------
<std>:699:11-64 function <format_codes_arr>

View File

@ -15,12 +15,12 @@ RUNTIME ERROR: Not enough values to format, got 1
std.toString(val)
-------------------------------------------------
<std>:558:9-26 builtin function <toString>
<std>:558:9-26 function <format_code>
std.toString(val)
-------------------------------------------------
... (skipped 14 frames)
... (skipped 10 frames)
-------------------------------------------------
<std>:699:11-64 function <format_codes_arr>

View File

@ -21,7 +21,7 @@ RUNTIME ERROR: Format required number at 0, got string
padding(w - std.length(str), s) + str;
-------------------------------------------------
... (skipped 11 frames)
... (skipped 7 frames)
-------------------------------------------------
<std>:699:11-64 function <format_codes_arr>

View File

@ -5,7 +5,7 @@ RUNTIME ERROR: x
std.filter(error "x", [])
-------------------------------------------------
testdata/std.filter2:1:1-26 builtin function <filter>
testdata/std.filter2:1:1-26 $
std.filter(error "x", [])

View File

@ -10,7 +10,7 @@ local failWith(x) = error x;
std.type(std.flatMap(failWith, ["a", "b", "c"]))
-------------------------------------------------
testdata/std.flatmap5:2:1-49 builtin function <type>
testdata/std.flatmap5:2:1-49 $
std.type(std.flatMap(failWith, ["a", "b", "c"]))

View File

@ -1,8 +1,8 @@
RUNTIME ERROR: Expected an integer, but got 1e+100
-------------------------------------------------
testdata/std.makeArray_noninteger_big:1:1-47 builtin function <makeArray>
testdata/std.makeArray_noninteger_big:1:1-36 builtin function <makeArray>
std.makeArray(1e100, error "shouldn't happen")
std.makeArray(1e100, function(i) i)
-------------------------------------------------
During evaluation

View File

@ -1 +1 @@
std.makeArray(1e100, error "shouldn't happen")
std.makeArray(1e100, function(i) i)

View File

@ -1,8 +1,6 @@
RUNTIME ERROR: max stack frames exceeded.
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-61 function <anonymous>
local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-57 function <anonymous>
@ -10,9 +8,7 @@ local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-61 function <anonymous>
local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-57 function <anonymous>
@ -22,9 +18,7 @@ local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
... (skipped 492 frames)
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-61 function <anonymous>
local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-57 function <anonymous>
@ -32,9 +26,7 @@ local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:51-61 function <anonymous>
local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500]
-------------------------------------------------
testdata/std.makeArray_recursive_evalutation_order_matters:1:64-72 $

View File

@ -5,7 +5,7 @@ RUNTIME ERROR: x
std.primitiveEquals(error "x", 42)
-------------------------------------------------
testdata/std.primitiveEquals10:1:1-35 builtin function <primitiveEquals>
testdata/std.primitiveEquals10:1:1-35 $
std.primitiveEquals(error "x", 42)

View File

@ -5,7 +5,7 @@ RUNTIME ERROR: x
std.primitiveEquals(42, error "x")
-------------------------------------------------
testdata/std.primitiveEquals9:1:1-35 builtin function <primitiveEquals>
testdata/std.primitiveEquals9:1:1-35 $
std.primitiveEquals(42, error "x")

View File

@ -5,7 +5,7 @@ RUNTIME ERROR: x
std.toString(error "x")
-------------------------------------------------
testdata/std.toString5:1:1-24 builtin function <toString>
testdata/std.toString5:1:1-24 $
std.toString(error "x")

View File

@ -5,7 +5,7 @@ RUNTIME ERROR: xxx
std.type(error "xxx")
-------------------------------------------------
testdata/type_error:1:1-22 builtin function <type>
testdata/type_error:1:1-22 $
std.type(error "xxx")

176
thunks.go
View File

@ -30,14 +30,10 @@ type readyValue struct {
content value
}
func (rv *readyValue) getValue(i *interpreter, t *TraceElement) (value, error) {
func (rv *readyValue) evaluate(i *interpreter, trace TraceElement, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error) {
return rv.content, nil
}
func (rv *readyValue) bindToObject(sb selfBinding, origBinding bindingFrame, fieldName string) potentialValue {
return rv
}
func (rv *readyValue) aPotentialValue() {}
// potentialValues
@ -48,108 +44,50 @@ func (rv *readyValue) aPotentialValue() {}
// potentialValue which guarantees that computation happens at most once).
type evaluable interface {
// fromWhere keeps the information from where the evaluation was requested.
getValue(i *interpreter, fromWhere *TraceElement) (value, error)
}
// thunk holds code and environment in which the code is supposed to be evaluated
type thunk struct {
env environment
body ast.Node
}
// TODO(sbarzowski) feedback from dcunnin:
// makeThunk returning a cachedThunk is weird.
// Maybe call thunk 'exprThunk' (or astThunk but then it looks like an AST node).
// Then call cachedThunk just thunk?
// Or, call this makeCachedExprThunk because that's what it really is.
func makeThunk(env environment, body ast.Node) *cachedThunk {
return makeCachedThunk(&thunk{
env: env,
body: body,
})
}
func (t *thunk) getValue(i *interpreter, trace *TraceElement) (value, error) {
return i.EvalInCleanEnv(trace, &t.env, t.body, false)
}
// callThunk represents a concrete, but not yet evaluated call to a function
type callThunk struct {
function evalCallable
args callArguments
}
func makeCallThunk(ec evalCallable, args callArguments) potentialValue {
return makeCachedThunk(&callThunk{function: ec, args: args})
}
func call(ec evalCallable, arguments ...potentialValue) potentialValue {
return makeCallThunk(ec, args(arguments...))
}
func (th *callThunk) getValue(i *interpreter, trace *TraceElement) (value, error) {
evaluator := makeEvaluator(i, trace)
err := checkArguments(evaluator, th.args, th.function.Parameters())
if err != nil {
return nil, err
}
return th.function.EvalCall(th.args, evaluator)
}
// deferredThunk allows deferring creation of evaluable until it's actually needed.
// It's useful for self-recursive structures.
type deferredThunk func() evaluable
func (th deferredThunk) getValue(i *interpreter, trace *TraceElement) (value, error) {
return th().getValue(i, trace)
}
func makeDeferredThunk(th deferredThunk) potentialValue {
return makeCachedThunk(th)
getValue(i *interpreter, fromWhere TraceElement) (value, error)
}
// cachedThunk is a wrapper that caches the value of a potentialValue after
// the first evaluation.
// Note: All potentialValues are required to provide the same value every time,
// so it's only there for efficiency.
// TODO(sbarzowski) investigate efficiency of various representations
type cachedThunk struct {
pv evaluable
// The environment is a pointer because it may be a cyclic structure. A thunk
// may refer to itself, so inside `env` there will be a variable bound back to us.
env *environment
body ast.Node
// If nil, use err.
content value
// If also nil, content is not cached yet.
err error
}
func makeCachedThunk(pv evaluable) *cachedThunk {
return &cachedThunk{pv}
func readyThunk(content value) *cachedThunk {
return &cachedThunk{content: content}
}
func (t *cachedThunk) getValue(i *interpreter, trace *TraceElement) (value, error) {
v, err := t.pv.getValue(i, trace)
func (t *cachedThunk) getValue(i *interpreter, trace TraceElement) (value, error) {
if t.content != nil {
return t.content, nil
}
if t.err != nil {
return nil, t.err
}
v, err := i.EvalInCleanEnv(trace, t.env, t.body, false)
if err != nil {
// TODO(sbarzowski) perhaps cache errors as well
// may be necessary if we allow handling them in any way
return nil, err
}
t.pv = &readyValue{v}
t.content = v
// No need to keep the environment around anymore.
// So, this might reduce memory pressure:
t.env = nil
return v, nil
}
func (t *cachedThunk) aPotentialValue() {}
// errorThunk can be used when potentialValue is expected, but we already
// know that something went wrong
type errorThunk struct {
err error
}
func (th *errorThunk) getValue(i *interpreter, trace *TraceElement) (value, error) {
return nil, th.err
}
func makeErrorThunk(err error) *errorThunk {
return &errorThunk{err}
}
func (th *errorThunk) aPotentialValue() {}
// unboundFields
// -------------------------------------
@ -157,9 +95,9 @@ type codeUnboundField struct {
body ast.Node
}
func (f *codeUnboundField) bindToObject(sb selfBinding, origBindings bindingFrame, fieldName string) potentialValue {
// TODO(sbarzowski) better object names (perhaps include a field name too?)
return makeThunk(makeEnvironment(origBindings, sb), f.body)
func (f *codeUnboundField) evaluate(i *interpreter, trace TraceElement, sb selfBinding, origBindings bindingFrame, fieldName string) (value, error) {
env := makeEnvironment(origBindings, sb)
return i.EvalInCleanEnv(trace, &env, f.body, false)
}
// Provide additional bindings for a field. It shadows bindings from the object.
@ -169,7 +107,7 @@ type bindingsUnboundField struct {
bindings bindingFrame
}
func (f *bindingsUnboundField) bindToObject(sb selfBinding, origBindings bindingFrame, fieldName string) potentialValue {
func (f *bindingsUnboundField) evaluate(i *interpreter, trace TraceElement, sb selfBinding, origBindings bindingFrame, fieldName string) (value, error) {
var upValues bindingFrame
upValues = make(bindingFrame)
for variable, pvalue := range origBindings {
@ -178,7 +116,7 @@ func (f *bindingsUnboundField) bindToObject(sb selfBinding, origBindings binding
for variable, pvalue := range f.bindings {
upValues[variable] = pvalue
}
return f.inner.bindToObject(sb, upValues, fieldName)
return f.inner.evaluate(i, trace, sb, upValues, fieldName)
}
// PlusSuperUnboundField represents a `field+: ...` that hasn't been bound to an object.
@ -186,13 +124,19 @@ type PlusSuperUnboundField struct {
inner unboundField
}
func (f *PlusSuperUnboundField) bindToObject(sb selfBinding, origBinding bindingFrame, fieldName string) potentialValue {
left := tryObjectIndex(sb.super(), fieldName, withHidden)
right := f.inner.bindToObject(sb, origBinding, fieldName)
if left != nil {
return call(bopBuiltins[ast.BopPlus], left, right)
func (f *PlusSuperUnboundField) evaluate(i *interpreter, trace TraceElement, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error) {
right, err := f.inner.evaluate(i, trace, sb, origBinding, fieldName)
if err != nil {
return nil, err
}
return right
if !objectHasField(sb.super(), fieldName, withHidden) {
return right, nil
}
left, err := objectIndex(i, trace, sb.super(), fieldName)
if err != nil {
return nil, err
}
return builtinPlus(i, trace, left, right)
}
// evalCallables
@ -206,9 +150,9 @@ type closure struct {
params Parameters
}
func forceThunks(e *evaluator, args bindingFrame) error {
for _, arg := range args {
_, err := e.evaluate(arg)
func forceThunks(i *interpreter, trace TraceElement, args *bindingFrame) error {
for _, arg := range *args {
_, err := arg.getValue(i, trace)
if err != nil {
return err
}
@ -216,7 +160,7 @@ func forceThunks(e *evaluator, args bindingFrame) error {
return nil
}
func (closure *closure) EvalCall(arguments callArguments, e *evaluator) (value, error) {
func (closure *closure) EvalCall(arguments callArguments, i *interpreter, trace TraceElement) (value, error) {
argThunks := make(bindingFrame)
parameters := closure.Parameters()
for i, arg := range arguments.positional {
@ -238,15 +182,16 @@ func (closure *closure) EvalCall(arguments callArguments, e *evaluator) (value,
for i := range parameters.optional {
param := &parameters.optional[i]
if _, exists := argThunks[param.name]; !exists {
argThunks[param.name] = makeDeferredThunk(func() evaluable {
argThunks[param.name] = &cachedThunk{
// Default arguments are evaluated in the same environment as function body
return param.defaultArg.inEnv(&calledEnvironment)
})
env: &calledEnvironment,
body: param.defaultArg,
}
}
}
if arguments.tailstrict {
err := forceThunks(e, argThunks)
err := forceThunks(i, trace, &argThunks)
if err != nil {
return nil, err
}
@ -254,9 +199,9 @@ func (closure *closure) EvalCall(arguments callArguments, e *evaluator) (value,
calledEnvironment = makeEnvironment(
addBindings(closure.env.upValues, argThunks),
closure.env.sb,
closure.env.selfBinding,
)
return e.evalInCleanEnv(&calledEnvironment, closure.function.Body, arguments.tailstrict)
return i.EvalInCleanEnv(trace, &calledEnvironment, closure.function.Body, arguments.tailstrict)
}
func (closure *closure) Parameters() Parameters {
@ -269,9 +214,7 @@ func prepareClosureParameters(parameters ast.Parameters, env environment) Parame
for _, named := range parameters.Optional {
optionalParameters = append(optionalParameters, namedParameter{
name: named.Name,
defaultArg: &defaultArgument{
body: named.DefaultArg,
},
defaultArg: named.DefaultArg,
})
}
return Parameters{
@ -296,15 +239,15 @@ type NativeFunction struct {
}
// EvalCall evaluates a call to a NativeFunction and returns the result.
func (native *NativeFunction) EvalCall(arguments callArguments, e *evaluator) (value, error) {
func (native *NativeFunction) EvalCall(arguments callArguments, i *interpreter, trace TraceElement) (value, error) {
flatArgs := flattenArgs(arguments, native.Parameters())
nativeArgs := make([]interface{}, 0, len(flatArgs))
for _, arg := range flatArgs {
v, err := e.evaluate(arg)
v, err := i.evaluatePV(arg, trace)
if err != nil {
return nil, err
}
json, err := e.i.manifestJSON(e.trace, v)
json, err := i.manifestJSON(trace, v)
if err != nil {
return nil, err
}
@ -312,9 +255,9 @@ func (native *NativeFunction) EvalCall(arguments callArguments, e *evaluator) (v
}
resultJSON, err := native.Func(nativeArgs)
if err != nil {
return nil, e.Error(err.Error())
return nil, i.Error(err.Error(), trace)
}
return jsonToValue(e, resultJSON)
return jsonToValue(i, trace, resultJSON)
}
// Parameters returns a NativeFunction's parameters.
@ -322,7 +265,6 @@ func (native *NativeFunction) Parameters() Parameters {
return Parameters{required: native.Params}
}
// partialPotentialValue
// -------------------------------------
type defaultArgument struct {
@ -330,5 +272,5 @@ type defaultArgument struct {
}
func (da *defaultArgument) inEnv(env *environment) potentialValue {
return makeThunk(*env, da.body)
return &cachedThunk{env: env, body: da.body}
}

128
value.go
View File

@ -58,13 +58,13 @@ var arrayType = &valueType{"array"}
// TODO(sbarzowski) perhaps call it just "Thunk"?
type potentialValue interface {
// fromWhere keeps the information from where the evaluation was requested.
getValue(i *interpreter, fromWhere *TraceElement) (value, error)
getValue(i *interpreter, fromWhere TraceElement) (value, error)
aPotentialValue()
}
// A set of variables with associated potentialValues.
type bindingFrame map[ast.Identifier]potentialValue
// A set of variables with associated thunks.
type bindingFrame map[ast.Identifier]*cachedThunk
type valueBase struct{}
@ -81,11 +81,11 @@ type valueString struct {
value []rune
}
func (s *valueString) index(e *evaluator, index int) (value, error) {
func (s *valueString) index(i *interpreter, trace TraceElement, index int) (value, error) {
if 0 <= index && index < s.length() {
return makeValueString(string(s.value[index])), nil
}
return nil, e.Error(fmt.Sprintf("Index %d out of bounds, not within [0, %v)", index, s.length()))
return nil, i.Error(fmt.Sprintf("Index %d out of bounds, not within [0, %v)", index, s.length()), trace)
}
func concatStrings(a, b *valueString) *valueString {
@ -199,28 +199,28 @@ func (*valueNull) getType() *valueType {
type valueArray struct {
valueBase
elements []potentialValue
elements []*cachedThunk
}
func (arr *valueArray) index(e *evaluator, index int, tc tailCallStatus) (value, error) {
func (arr *valueArray) index(i *interpreter, trace TraceElement, index int) (value, error) {
if 0 <= index && index < arr.length() {
return e.evaluateTailCall(arr.elements[index], tc)
return i.evaluatePV(arr.elements[index], trace)
}
return nil, e.Error(fmt.Sprintf("Index %d out of bounds, not within [0, %v)", index, arr.length()))
return nil, i.Error(fmt.Sprintf("Index %d out of bounds, not within [0, %v)", index, arr.length()), trace)
}
func (arr *valueArray) length() int {
return len(arr.elements)
}
func makeValueArray(elements []potentialValue) *valueArray {
func makeValueArray(elements []*cachedThunk) *valueArray {
// We don't want to keep a bigger array than necessary
// so we create a new one with minimal capacity
var arrayElems []potentialValue
var arrayElems []*cachedThunk
if len(elements) == cap(elements) {
arrayElems = elements
} else {
arrayElems = make([]potentialValue, len(elements))
arrayElems = make([]*cachedThunk, len(elements))
for i := range elements {
arrayElems[i] = elements[i]
}
@ -231,7 +231,7 @@ func makeValueArray(elements []potentialValue) *valueArray {
}
func concatArrays(a, b *valueArray) *valueArray {
result := make([]potentialValue, 0, len(a.elements)+len(b.elements))
result := make([]*cachedThunk, 0, len(a.elements)+len(b.elements))
for _, r := range a.elements {
result = append(result, r)
}
@ -255,23 +255,23 @@ type valueFunction struct {
// TODO(sbarzowski) better name?
type evalCallable interface {
EvalCall(args callArguments, e *evaluator) (value, error)
EvalCall(args callArguments, i *interpreter, trace TraceElement) (value, error)
Parameters() Parameters
}
type partialPotentialValue interface {
inEnv(env *environment) potentialValue
func (f *valueFunction) call(i *interpreter, trace TraceElement, args callArguments) (value, error) {
err := checkArguments(i, trace, args, f.parameters())
if err != nil {
return nil, err
}
func (f *valueFunction) call(args callArguments) potentialValue {
return makeCallThunk(f.ec, args)
return f.ec.EvalCall(args, i, trace)
}
func (f *valueFunction) parameters() Parameters {
return f.ec.Parameters()
}
func checkArguments(e *evaluator, args callArguments, params Parameters) error {
func checkArguments(i *interpreter, trace TraceElement, args callArguments, params Parameters) error {
received := make(map[ast.Identifier]bool)
accepted := make(map[ast.Identifier]bool)
@ -279,7 +279,7 @@ func checkArguments(e *evaluator, args callArguments, params Parameters) error {
numExpected := len(params.required) + len(params.optional)
if numPassed > numExpected {
return e.Error(fmt.Sprintf("function expected %v positional argument(s), but got %v", numExpected, numPassed))
return i.Error(fmt.Sprintf("function expected %v positional argument(s), but got %v", numExpected, numPassed), trace)
}
for _, param := range params.required {
@ -300,17 +300,17 @@ func checkArguments(e *evaluator, args callArguments, params Parameters) error {
for _, arg := range args.named {
if _, present := received[arg.name]; present {
return e.Error(fmt.Sprintf("Argument %v already provided", arg.name))
return i.Error(fmt.Sprintf("Argument %v already provided", arg.name), trace)
}
if _, present := accepted[arg.name]; !present {
return e.Error(fmt.Sprintf("function has no parameter %v", arg.name))
return i.Error(fmt.Sprintf("function has no parameter %v", arg.name), trace)
}
received[arg.name] = true
}
for _, param := range params.required {
if _, present := received[param]; !present {
return e.Error(fmt.Sprintf("Missing argument: %v", param))
return i.Error(fmt.Sprintf("Missing argument: %v", param), trace)
}
}
@ -330,25 +330,25 @@ type Parameters struct {
type namedParameter struct {
name ast.Identifier
defaultArg potentialValueInEnv
defaultArg ast.Node
}
type potentialValueInEnv interface {
inEnv(env *environment) potentialValue
inEnv(env *environment) *cachedThunk
}
type callArguments struct {
positional []potentialValue
positional []*cachedThunk
named []namedCallArgument
tailstrict bool
}
type namedCallArgument struct {
name ast.Identifier
pv potentialValue
pv *cachedThunk
}
func args(xs ...potentialValue) callArguments {
func args(xs ...*cachedThunk) callArguments {
return callArguments{positional: xs}
}
@ -363,7 +363,7 @@ func args(xs ...potentialValue) callArguments {
type valueObject interface {
value
inheritanceSize() int
index(e *evaluator, field string) (value, error)
index(i *interpreter, trace TraceElement, field string) (value, error)
assertionsChecked() bool
setAssertionsCheckResult(err error)
getAssertionsCheckResult() error
@ -471,21 +471,22 @@ type valueSimpleObject struct {
asserts []unboundField
}
func checkAssertionsHelper(e *evaluator, obj valueObject, curr valueObject, superDepth int) error {
func checkAssertionsHelper(i *interpreter, trace TraceElement, obj valueObject, curr valueObject, superDepth int) error {
switch curr := curr.(type) {
case *valueExtendedObject:
err := checkAssertionsHelper(e, obj, curr.right, superDepth)
err := checkAssertionsHelper(i, trace, obj, curr.right, superDepth)
if err != nil {
return err
}
err = checkAssertionsHelper(e, obj, curr.left, superDepth+curr.right.inheritanceSize())
err = checkAssertionsHelper(i, trace, obj, curr.left, superDepth+curr.right.inheritanceSize())
if err != nil {
return err
}
return nil
case *valueSimpleObject:
for _, assert := range curr.asserts {
_, err := e.evaluate(assert.bindToObject(selfBinding{self: obj, superDepth: superDepth}, curr.upValues, ""))
sb := selfBinding{self: obj, superDepth: superDepth}
_, err := assert.evaluate(i, trace, sb, curr.upValues, "")
if err != nil {
return err
}
@ -496,19 +497,19 @@ func checkAssertionsHelper(e *evaluator, obj valueObject, curr valueObject, supe
}
}
func checkAssertions(e *evaluator, obj valueObject) error {
func checkAssertions(i *interpreter, trace TraceElement, obj valueObject) error {
if !obj.assertionsChecked() {
// Assertions may refer to the object that will normally
// trigger checking of assertions, resulting in an endless recursion.
// To avoid that, while we check them, we treat them as already passed.
obj.setAssertionsCheckResult(errNoErrorInObjectInvariants)
obj.setAssertionsCheckResult(checkAssertionsHelper(e, obj, obj, 0))
obj.setAssertionsCheckResult(checkAssertionsHelper(i, trace, obj, obj, 0))
}
return obj.getAssertionsCheckResult()
}
func (o *valueSimpleObject) index(e *evaluator, field string) (value, error) {
return objectIndex(e, objectBinding(o), field)
func (o *valueSimpleObject) index(i *interpreter, trace TraceElement, field string) (value, error) {
return objectIndex(i, trace, objectBinding(o), field)
}
func (*valueSimpleObject) inheritanceSize() int {
@ -532,7 +533,7 @@ type simpleObjectField struct {
// unboundField is a field that doesn't know yet in which object it is.
type unboundField interface {
bindToObject(sb selfBinding, origBinding bindingFrame, fieldName string) potentialValue
evaluate(i *interpreter, trace TraceElement, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error)
}
// valueExtendedObject represents an object created through inheritence (left + right).
@ -559,8 +560,8 @@ type valueExtendedObject struct {
totalInheritanceSize int
}
func (o *valueExtendedObject) index(e *evaluator, field string) (value, error) {
return objectIndex(e, objectBinding(o), field)
func (o *valueExtendedObject) index(i *interpreter, trace TraceElement, field string) (value, error) {
return objectIndex(i, trace, objectBinding(o), field)
}
func (o *valueExtendedObject) inheritanceSize() int {
@ -578,53 +579,54 @@ func makeValueExtendedObject(left, right valueObject) *valueExtendedObject {
// findField returns a field in object curr, with superDepth at least minSuperDepth
// It also returns an associated bindingFrame and actual superDepth that the field
// was found at.
func findField(curr value, minSuperDepth int, f string) (*simpleObjectField, bindingFrame, int) {
func findField(curr value, minSuperDepth int, f string) (bool, simpleObjectField, bindingFrame, int) {
switch curr := curr.(type) {
case *valueExtendedObject:
if curr.right.inheritanceSize() > minSuperDepth {
field, frame, counter := findField(curr.right, minSuperDepth, f)
if field != nil {
return field, frame, counter
found, field, frame, counter := findField(curr.right, minSuperDepth, f)
if found {
return true, field, frame, counter
}
}
field, frame, counter := findField(curr.left, minSuperDepth-curr.right.inheritanceSize(), f)
return field, frame, counter + curr.right.inheritanceSize()
found, field, frame, counter := findField(curr.left, minSuperDepth-curr.right.inheritanceSize(), f)
return found, field, frame, counter + curr.right.inheritanceSize()
case *valueSimpleObject:
if minSuperDepth <= 0 {
if field, ok := curr.fields[f]; ok {
return &field, curr.upValues, 0
return true, field, curr.upValues, 0
}
}
return nil, nil, 0
return false, simpleObjectField{}, nil, 0
default:
panic(fmt.Sprintf("Unknown object type %#v", curr))
}
}
func objectIndex(e *evaluator, sb selfBinding, fieldName string) (value, error) {
err := checkAssertions(e, sb.self)
func objectIndex(i *interpreter, trace TraceElement, sb selfBinding, fieldName string) (value, error) {
err := checkAssertions(i, trace, sb.self)
if err != nil {
return nil, err
}
if sb.superDepth >= sb.self.inheritanceSize() {
return nil, e.Error("Attempt to use super when there is no super class.")
}
objp := tryObjectIndex(sb, fieldName, withHidden)
if objp == nil {
return nil, e.Error(fmt.Sprintf("Field does not exist: %s", fieldName))
}
return e.evaluate(objp)
return nil, i.Error("Attempt to use super when there is no super class.", trace)
}
func tryObjectIndex(sb selfBinding, fieldName string, h Hidden) potentialValue {
field, upValues, foundAt := findField(sb.self, sb.superDepth, fieldName)
if field == nil || (h == withoutHidden && field.hide == ast.ObjectFieldHidden) {
return nil
found, field, upValues, foundAt := findField(sb.self, sb.superDepth, fieldName)
if !found {
return nil, i.Error(fmt.Sprintf("Field does not exist: %s", fieldName), trace)
}
fieldSelfBinding := selfBinding{self: sb.self, superDepth: foundAt}
return field.field.evaluate(i, trace, fieldSelfBinding, upValues, fieldName)
}
return field.field.bindToObject(fieldSelfBinding, upValues, fieldName)
func objectHasField(sb selfBinding, fieldName string, h Hidden) bool {
found, field, _, _ := findField(sb.self, sb.superDepth, fieldName)
if !found || (h == withoutHidden && field.hide == ast.ObjectFieldHidden) {
return false
}
return true
}
type fieldHideMap map[string]ast.ObjectFieldHide