From 2973e24152b2f0721311a1f3018ae7246d23d103 Mon Sep 17 00:00:00 2001 From: Dave Cunningham Date: Fri, 1 Jun 2018 10:52:20 -0400 Subject: [PATCH] Internal refactor to improve performance (#225) * Internal refactor to improve performance --- builtins.go | 537 ++++++++---------- evaluator.go | 227 -------- imports.go | 23 +- interpreter.go | 522 ++++++++++++----- runtime_error.go | 2 +- testdata/bitwise_and4.golden | 5 - testdata/bitwise_xor7.golden | 5 - testdata/lazy_operator2.golden | 5 - testdata/object_invariant7.golden | 5 - testdata/or4.golden | 5 - testdata/percent_format_str5.golden | 4 +- testdata/percent_format_str6.golden | 4 +- testdata/percent_format_str7.golden | 2 +- testdata/std.filter2.golden | 2 +- testdata/std.flatmap5.golden | 2 +- testdata/std.makeArray_noninteger_big.golden | 4 +- testdata/std.makeArray_noninteger_big.jsonnet | 2 +- ...recursive_evalutation_order_matters.golden | 16 +- testdata/std.primitiveEquals10.golden | 2 +- testdata/std.primitiveEquals9.golden | 2 +- testdata/std.toString5.golden | 2 +- testdata/type_error.golden | 2 +- thunks.go | 178 ++---- value.go | 130 ++--- 24 files changed, 776 insertions(+), 912 deletions(-) delete mode 100644 evaluator.go diff --git a/builtins.go b/builtins.go index 03fa3de80..d44792c82 100644 --- a/builtins.go +++ b/builtins.go @@ -29,20 +29,12 @@ import ( "github.com/google/go-jsonnet/ast" ) -func builtinPlus(e *evaluator, xp, yp potentialValue) (value, error) { +func builtinPlus(i *interpreter, trace TraceElement, x, y value) (value, error) { // TODO(sbarzowski) more types, mixing types // TODO(sbarzowski) perhaps a more elegant way to dispatch - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } - y, err := e.evaluate(yp) - if err != nil { - return nil, err - } switch right := y.(type) { case *valueString: - left, err := builtinToString(e, xp) + left, err := builtinToString(i, trace, x) if err != nil { return nil, err } @@ -51,166 +43,130 @@ func builtinPlus(e *evaluator, xp, yp potentialValue) (value, error) { } switch left := x.(type) { case *valueNumber: - right, err := e.getNumber(y) + right, err := i.getNumber(y, trace) if err != nil { return nil, err } return makeValueNumber(left.value + right.value), nil case *valueString: - right, err := builtinToString(e, yp) + right, err := builtinToString(i, trace, y) if err != nil { return nil, err } return concatStrings(left, right.(*valueString)), nil case valueObject: - right, err := e.getObject(y) - if err != nil { - return nil, err + switch right := y.(type) { + case valueObject: + return makeValueExtendedObject(left, right), nil + default: + return nil, i.typeErrorSpecific(y, &valueSimpleObject{}, trace) } - return makeValueExtendedObject(left, right), nil + case *valueArray: - right, err := e.getArray(y) + right, err := i.getArray(y, trace) if err != nil { return nil, err } return concatArrays(left, right), nil default: - return nil, e.typeErrorGeneral(x) + return nil, i.typeErrorGeneral(x, trace) } } -func builtinMinus(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinMinus(i *interpreter, trace TraceElement, xv, yv value) (value, error) { + x, err := i.getNumber(xv, trace) if err != nil { return nil, err } - y, err := e.evaluateNumber(yp) + y, err := i.getNumber(yv, trace) if err != nil { return nil, err } return makeValueNumber(x.value - y.value), nil } -func builtinMult(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinMult(i *interpreter, trace TraceElement, xv, yv value) (value, error) { + x, err := i.getNumber(xv, trace) if err != nil { return nil, err } - y, err := e.evaluateNumber(yp) + y, err := i.getNumber(yv, trace) if err != nil { return nil, err } return makeValueNumber(x.value * y.value), nil } -func builtinDiv(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinDiv(i *interpreter, trace TraceElement, xv, yv value) (value, error) { + x, err := i.getNumber(xv, trace) if err != nil { return nil, err } - y, err := e.evaluateNumber(yp) + y, err := i.getNumber(yv, trace) if err != nil { return nil, err } if y.value == 0 { - return nil, e.Error("Division by zero.") + return nil, i.Error("Division by zero.", trace) } - return makeDoubleCheck(e, x.value/y.value) + return makeDoubleCheck(i, trace, x.value/y.value) } -func builtinModulo(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinModulo(i *interpreter, trace TraceElement, xv, yv value) (value, error) { + x, err := i.getNumber(xv, trace) if err != nil { return nil, err } - y, err := e.evaluateNumber(yp) + y, err := i.getNumber(yv, trace) if err != nil { return nil, err } if y.value == 0 { - return nil, e.Error("Division by zero.") + return nil, i.Error("Division by zero.", trace) } - return makeDoubleCheck(e, math.Mod(x.value, y.value)) + return makeDoubleCheck(i, trace, math.Mod(x.value, y.value)) } -func builtinLess(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } +func builtinLess(i *interpreter, trace TraceElement, x, yv value) (value, error) { switch left := x.(type) { case *valueNumber: - right, err := e.evaluateNumber(yp) + right, err := i.getNumber(yv, trace) if err != nil { return nil, err } return makeValueBoolean(left.value < right.value), nil case *valueString: - right, err := e.evaluateString(yp) + right, err := i.getString(yv, trace) if err != nil { return nil, err } return makeValueBoolean(stringLessThan(left, right)), nil default: - return nil, e.typeErrorGeneral(x) + return nil, i.typeErrorGeneral(x, trace) } } -func builtinGreater(e *evaluator, xp, yp potentialValue) (value, error) { - return builtinLess(e, yp, xp) +func builtinGreater(i *interpreter, trace TraceElement, x, y value) (value, error) { + return builtinLess(i, trace, y, x) } -func builtinGreaterEq(e *evaluator, xp, yp potentialValue) (value, error) { - res, err := builtinLess(e, xp, yp) +func builtinGreaterEq(i *interpreter, trace TraceElement, x, y value) (value, error) { + res, err := builtinLess(i, trace, x, y) if err != nil { return nil, err } return res.(*valueBoolean).not(), nil } -func builtinLessEq(e *evaluator, xp, yp potentialValue) (value, error) { - res, err := builtinGreater(e, xp, yp) +func builtinLessEq(i *interpreter, trace TraceElement, x, y value) (value, error) { + res, err := builtinGreater(i, trace, x, y) if err != nil { return nil, err } return res.(*valueBoolean).not(), nil } -func builtinAnd(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateBoolean(xp) - if err != nil { - return nil, err - } - if !x.value { - return x, nil - } - y, err := e.evaluateBoolean(yp) - if err != nil { - return nil, err - } - return y, nil -} - -func builtinOr(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateBoolean(xp) - if err != nil { - return nil, err - } - if x.value { - return x, nil - } - y, err := e.evaluateBoolean(yp) - if err != nil { - return nil, err - } - return y, nil -} - -func builtinLength(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } +func builtinLength(i *interpreter, trace TraceElement, x value) (value, error) { var num int switch x := x.(type) { case valueObject: @@ -222,67 +178,76 @@ func builtinLength(e *evaluator, xp potentialValue) (value, error) { case *valueFunction: num = len(x.parameters().required) default: - return nil, e.typeErrorGeneral(x) + return nil, i.typeErrorGeneral(x, trace) } return makeValueNumber(float64(num)), nil } -func builtinToString(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } +func builtinToString(i *interpreter, trace TraceElement, x value) (value, error) { switch x := x.(type) { case *valueString: return x, nil } var buf bytes.Buffer - err = e.i.manifestAndSerializeJSON(&buf, e.trace, x, false, "") + err := i.manifestAndSerializeJSON(&buf, trace, x, false, "") if err != nil { return nil, err } return makeValueString(buf.String()), nil } -func builtinTrace(e *evaluator, xp potentialValue, yp potentialValue) (value, error) { - x, err := e.evaluateString(xp) - if err != nil { - return nil, err - } - y, err := e.evaluate(yp) +func builtinTrace(i *interpreter, trace TraceElement, x value, y value) (value, error) { + xStr, err := i.getString(x, trace) if err != nil { return nil, err } - filename := e.trace.loc.FileName - line := e.trace.loc.Begin.Line + filename := trace.loc.FileName + line := trace.loc.Begin.Line fmt.Fprintf( - os.Stderr, "TRACE: %s:%d %s\n", filename, line, x.getString()) + os.Stderr, "TRACE: %s:%d %s\n", filename, line, xStr.getString()) return y, nil } -func builtinMakeArray(e *evaluator, szp potentialValue, funcp potentialValue) (value, error) { - sz, err := e.evaluateInt(szp) +// astMakeArrayElement wraps the function argument of std.makeArray so that +// it can be embedded in cachedThunk without needing to execute it ahead of +// time. It is equivalent to `local i = 42; func(i)`. It therefore has no +// free variables and needs only an empty environment to execute. +type astMakeArrayElement struct { + ast.NodeBase + function *valueFunction + index int +} + +func builtinMakeArray(i *interpreter, trace TraceElement, szv, funcv value) (value, error) { + sz, err := i.getInt(szv, trace) if err != nil { return nil, err } - fun, err := e.evaluateFunction(funcp) + fun, err := i.getFunction(funcv, trace) if err != nil { return nil, err } - var elems []potentialValue + var elems []*cachedThunk for i := 0; i < sz; i++ { - elem := fun.call(args(&readyValue{intToValue(i)})) + elem := &cachedThunk{ + env: &environment{}, + body: &astMakeArrayElement{ + NodeBase: ast.NodeBase{}, + function: fun, + index: i, + }, + } elems = append(elems, elem) } return makeValueArray(elems), nil } -func builtinFlatMap(e *evaluator, funcp potentialValue, arrp potentialValue) (value, error) { - arr, err := e.evaluateArray(arrp) +func builtinFlatMap(i *interpreter, trace TraceElement, funcv, arrv value) (value, error) { + arr, err := i.getArray(arrv, trace) if err != nil { return nil, err } - fun, err := e.evaluateFunction(funcp) + fun, err := i.getFunction(funcv, trace) if err != nil { return nil, err } @@ -290,9 +255,13 @@ func builtinFlatMap(e *evaluator, funcp potentialValue, arrp potentialValue) (va // Start with capacity of the original array. // This may spare us a few reallocations. // TODO(sbarzowski) verify that it actually helps - elems := make([]potentialValue, 0, num) - for i := 0; i < num; i++ { - returned, err := e.evaluateArray(fun.call(args(arr.elements[i]))) + elems := make([]*cachedThunk, 0, num) + for counter := 0; counter < num; counter++ { + returnedValue, err := fun.call(i, trace, args(arr.elements[counter])) + if err != nil { + return nil, err + } + returned, err := i.getArray(returnedValue, trace) if err != nil { return nil, err } @@ -303,11 +272,11 @@ func builtinFlatMap(e *evaluator, funcp potentialValue, arrp potentialValue) (va return makeValueArray(elems), nil } -func joinArrays(e *evaluator, sep *valueArray, arr *valueArray) (value, error) { - result := make([]potentialValue, 0, arr.length()) +func joinArrays(i *interpreter, trace TraceElement, sep *valueArray, arr *valueArray) (value, error) { + result := make([]*cachedThunk, 0, arr.length()) first := true for _, elem := range arr.elements { - elemValue, err := e.evaluate(elem) + elemValue, err := i.evaluatePV(elem, trace) if err != nil { return nil, err } @@ -324,7 +293,7 @@ func joinArrays(e *evaluator, sep *valueArray, arr *valueArray) (value, error) { result = append(result, subElem) } default: - return nil, e.typeErrorSpecific(elemValue, &valueArray{}) + return nil, i.typeErrorSpecific(elemValue, &valueArray{}, trace) } first = false @@ -332,11 +301,11 @@ func joinArrays(e *evaluator, sep *valueArray, arr *valueArray) (value, error) { return makeValueArray(result), nil } -func joinStrings(e *evaluator, sep *valueString, arr *valueArray) (value, error) { +func joinStrings(i *interpreter, trace TraceElement, sep *valueString, arr *valueArray) (value, error) { result := make([]rune, 0, arr.length()) first := true for _, elem := range arr.elements { - elemValue, err := e.evaluate(elem) + elemValue, err := i.evaluatePV(elem, trace) if err != nil { return nil, err } @@ -349,38 +318,34 @@ func joinStrings(e *evaluator, sep *valueString, arr *valueArray) (value, error) } result = append(result, v.value...) default: - return nil, e.typeErrorSpecific(elemValue, &valueString{}) + return nil, i.typeErrorSpecific(elemValue, &valueString{}, trace) } first = false } return &valueString{value: result}, nil } -func builtinJoin(e *evaluator, sepp potentialValue, arrp potentialValue) (value, error) { - arr, err := e.evaluateArray(arrp) - if err != nil { - return nil, err - } - sep, err := e.evaluate(sepp) +func builtinJoin(i *interpreter, trace TraceElement, sep, arrv value) (value, error) { + arr, err := i.getArray(arrv, trace) if err != nil { return nil, err } switch sep := sep.(type) { case *valueString: - return joinStrings(e, sep, arr) + return joinStrings(i, trace, sep, arr) case *valueArray: - return joinArrays(e, sep, arr) + return joinArrays(i, trace, sep, arr) default: - return nil, e.Error("join first parameter should be string or array, got " + sep.getType().name) + return nil, i.Error("join first parameter should be string or array, got "+sep.getType().name, trace) } } -func builtinFilter(e *evaluator, funcp potentialValue, arrp potentialValue) (value, error) { - arr, err := e.evaluateArray(arrp) +func builtinFilter(i *interpreter, trace TraceElement, funcv, arrv value) (value, error) { + arr, err := i.getArray(arrv, trace) if err != nil { return nil, err } - fun, err := e.evaluateFunction(funcp) + fun, err := i.getFunction(funcv, trace) if err != nil { return nil, err } @@ -388,97 +353,89 @@ func builtinFilter(e *evaluator, funcp potentialValue, arrp potentialValue) (val // Start with capacity of the original array. // This may spare us a few reallocations. // TODO(sbarzowski) verify that it actually helps - elems := make([]potentialValue, 0, num) - for i := 0; i < num; i++ { - included, err := e.evaluateBoolean(fun.call(args(arr.elements[i]))) + elems := make([]*cachedThunk, 0, num) + for counter := 0; counter < num; counter++ { + includedValue, err := fun.call(i, trace, args(arr.elements[counter])) + if err != nil { + return nil, err + } + included, err := i.getBoolean(includedValue, trace) if err != nil { return nil, err } if included.value { - elems = append(elems, arr.elements[i]) + elems = append(elems, arr.elements[counter]) } } return makeValueArray(elems), nil } -func builtinRange(e *evaluator, fromp potentialValue, top potentialValue) (value, error) { - from, err := e.evaluateInt(fromp) +func builtinRange(i *interpreter, trace TraceElement, fromv, tov value) (value, error) { + from, err := i.getInt(fromv, trace) if err != nil { return nil, err } - to, err := e.evaluateInt(top) + to, err := i.getInt(tov, trace) if err != nil { return nil, err } - elems := make([]potentialValue, to-from+1) + elems := make([]*cachedThunk, to-from+1) for i := from; i <= to; i++ { - elems[i-from] = &readyValue{intToValue(i)} + elems[i-from] = readyThunk(intToValue(i)) } return makeValueArray(elems), nil } -func builtinNegation(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateBoolean(xp) +func builtinNegation(i *interpreter, trace TraceElement, x value) (value, error) { + b, err := i.getBoolean(x, trace) if err != nil { return nil, err } - return makeValueBoolean(!x.value), nil + return makeValueBoolean(!b.value), nil } -func builtinBitNeg(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinBitNeg(i *interpreter, trace TraceElement, x value) (value, error) { + n, err := i.getNumber(x, trace) if err != nil { return nil, err } - i := int64(x.value) - return int64ToValue(^i), nil + intValue := int64(n.value) + return int64ToValue(^intValue), nil } -func builtinIdentity(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } +func builtinIdentity(i *interpreter, trace TraceElement, x value) (value, error) { return x, nil } -func builtinUnaryMinus(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinUnaryMinus(i *interpreter, trace TraceElement, x value) (value, error) { + n, err := i.getNumber(x, trace) if err != nil { return nil, err } - return makeValueNumber(-x.value), nil + return makeValueNumber(-n.value), nil } // TODO(sbarzowski) since we have a builtin implementation of equals it's no longer really // needed and we should deprecate it eventually -func primitiveEquals(e *evaluator, xp potentialValue, yp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } - y, err := e.evaluate(yp) - if err != nil { - return nil, err - } +func primitiveEquals(i *interpreter, trace TraceElement, x, y value) (value, error) { if x.getType() != y.getType() { return makeValueBoolean(false), nil } switch left := x.(type) { case *valueBoolean: - right, err := e.getBoolean(y) + right, err := i.getBoolean(y, trace) if err != nil { return nil, err } return makeValueBoolean(left.value == right.value), nil case *valueNumber: - right, err := e.getNumber(y) + right, err := i.getNumber(y, trace) if err != nil { return nil, err } return makeValueBoolean(left.value == right.value), nil case *valueString: - right, err := e.getString(y) + right, err := i.getString(y, trace) if err != nil { return nil, err } @@ -486,33 +443,34 @@ func primitiveEquals(e *evaluator, xp potentialValue, yp potentialValue) (value, case *valueNull: return makeValueBoolean(true), nil case *valueFunction: - return nil, e.Error("Cannot test equality of functions") + return nil, i.Error("Cannot test equality of functions", trace) default: - return nil, e.Error( - "primitiveEquals operates on primitive types, got " + x.getType().name, + return nil, i.Error( + "primitiveEquals operates on primitive types, got "+x.getType().name, + trace, ) } } -func rawEquals(e *evaluator, x, y value) (bool, error) { +func rawEquals(i *interpreter, trace TraceElement, x, y value) (bool, error) { if x.getType() != y.getType() { return false, nil } switch left := x.(type) { case *valueBoolean: - right, err := e.getBoolean(y) + right, err := i.getBoolean(y, trace) if err != nil { return false, err } return left.value == right.value, nil case *valueNumber: - right, err := e.getNumber(y) + right, err := i.getNumber(y, trace) if err != nil { return false, err } return left.value == right.value, nil case *valueString: - right, err := e.getString(y) + right, err := i.getString(y, trace) if err != nil { return false, err } @@ -520,23 +478,23 @@ func rawEquals(e *evaluator, x, y value) (bool, error) { case *valueNull: return true, nil case *valueArray: - right, err := e.getArray(y) + right, err := i.getArray(y, trace) if err != nil { return false, err } if left.length() != right.length() { return false, nil } - for i := range left.elements { - leftElem, err := e.evaluate(left.elements[i]) + for j := range left.elements { + leftElem, err := i.evaluatePV(left.elements[j], trace) if err != nil { return false, err } - rightElem, err := e.evaluate(right.elements[i]) + rightElem, err := i.evaluatePV(right.elements[j], trace) if err != nil { return false, err } - eq, err := rawEquals(e, leftElem, rightElem) + eq, err := rawEquals(i, trace, leftElem, rightElem) if err != nil { return false, err } @@ -546,7 +504,7 @@ func rawEquals(e *evaluator, x, y value) (bool, error) { } return true, nil case valueObject: - right, err := e.getObject(y) + right, err := i.getObject(y, trace) if err != nil { return false, err } @@ -562,17 +520,17 @@ func rawEquals(e *evaluator, x, y value) (bool, error) { return false, nil } } - for i := range leftFields { - fieldName := leftFields[i] - leftField, err := left.index(e, fieldName) + for j := range leftFields { + fieldName := leftFields[j] + leftField, err := left.index(i, trace, fieldName) if err != nil { return false, err } - rightField, err := right.index(e, fieldName) + rightField, err := right.index(i, trace, fieldName) if err != nil { return false, err } - eq, err := rawEquals(e, leftField, rightField) + eq, err := rawEquals(i, trace, leftField, rightField) if err != nil { return false, err } @@ -582,57 +540,37 @@ func rawEquals(e *evaluator, x, y value) (bool, error) { } return true, nil case *valueFunction: - return false, e.Error("Cannot test equality of functions") + return false, i.Error("Cannot test equality of functions", trace) } panic(fmt.Sprintf("Unhandled case in equals %#+v %#+v", x, y)) } -func builtinEquals(e *evaluator, xp potentialValue, yp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } - y, err := e.evaluate(yp) - if err != nil { - return nil, err - } - eq, err := rawEquals(e, x, y) +func builtinEquals(i *interpreter, trace TraceElement, x, y value) (value, error) { + eq, err := rawEquals(i, trace, x, y) if err != nil { return nil, err } return makeValueBoolean(eq), nil } -func builtinNotEquals(e *evaluator, xp potentialValue, yp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } - y, err := e.evaluate(yp) - if err != nil { - return nil, err - } - eq, err := rawEquals(e, x, y) +func builtinNotEquals(i *interpreter, trace TraceElement, x, y value) (value, error) { + eq, err := rawEquals(i, trace, x, y) if err != nil { return nil, err } return makeValueBoolean(!eq), nil } -func builtinType(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluate(xp) - if err != nil { - return nil, err - } +func builtinType(i *interpreter, trace TraceElement, x value) (value, error) { return makeValueString(x.getType().name), nil } -func builtinMd5(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateString(xp) +func builtinMd5(i *interpreter, trace TraceElement, x value) (value, error) { + str, err := i.getString(x, trace) if err != nil { return nil, err } - hash := md5.Sum([]byte(string(x.value))) + hash := md5.Sum([]byte(string(str.value))) return makeValueString(hex.EncodeToString(hash[:])), nil } @@ -640,47 +578,47 @@ func builtinMd5(e *evaluator, xp potentialValue) (value, error) { // https://en.wikipedia.org/wiki/Unicode#Architecture_and_terminology const codepointMax = 0x10FFFF -func builtinChar(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func builtinChar(i *interpreter, trace TraceElement, x value) (value, error) { + n, err := i.getNumber(x, trace) if err != nil { return nil, err } - if x.value > codepointMax { - return nil, e.Error(fmt.Sprintf("Invalid unicode codepoint, got %v", x.value)) - } else if x.value < 0 { - return nil, e.Error(fmt.Sprintf("Codepoints must be >= 0, got %v", x.value)) + if n.value > codepointMax { + return nil, i.Error(fmt.Sprintf("Invalid unicode codepoint, got %v", n.value), trace) + } else if n.value < 0 { + return nil, i.Error(fmt.Sprintf("Codepoints must be >= 0, got %v", n.value), trace) } - return makeValueString(string(rune(x.value))), nil + return makeValueString(string(rune(n.value))), nil } -func builtinCodepoint(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateString(xp) +func builtinCodepoint(i *interpreter, trace TraceElement, x value) (value, error) { + str, err := i.getString(x, trace) if err != nil { return nil, err } - if x.length() != 1 { - return nil, e.Error(fmt.Sprintf("codepoint takes a string of length 1, got length %v", x.length())) + if str.length() != 1 { + return nil, i.Error(fmt.Sprintf("codepoint takes a string of length 1, got length %v", str.length()), trace) } - return makeValueNumber(float64(x.value[0])), nil + return makeValueNumber(float64(str.value[0])), nil } -func makeDoubleCheck(e *evaluator, x float64) (value, error) { +func makeDoubleCheck(i *interpreter, trace TraceElement, x float64) (value, error) { if math.IsNaN(x) { - return nil, e.Error("Not a number") + return nil, i.Error("Not a number", trace) } if math.IsInf(x, 0) { - return nil, e.Error("Overflow") + return nil, i.Error("Overflow", trace) } return makeValueNumber(x), nil } -func liftNumeric(f func(float64) float64) func(*evaluator, potentialValue) (value, error) { - return func(e *evaluator, xp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func liftNumeric(f func(float64) float64) func(*interpreter, TraceElement, value) (value, error) { + return func(i *interpreter, trace TraceElement, x value) (value, error) { + n, err := i.getNumber(x, trace) if err != nil { return nil, err } - return makeDoubleCheck(e, f(x.value)) + return makeDoubleCheck(i, trace, f(n.value)) } } @@ -710,17 +648,17 @@ var builtinExponent = liftNumeric(func(f float64) float64 { return float64(exponent) }) -func liftBitwise(f func(int64, int64) int64) func(*evaluator, potentialValue, potentialValue) (value, error) { - return func(e *evaluator, xp, yp potentialValue) (value, error) { - x, err := e.evaluateNumber(xp) +func liftBitwise(f func(int64, int64) int64) func(*interpreter, TraceElement, value, value) (value, error) { + return func(i *interpreter, trace TraceElement, xv, yv value) (value, error) { + x, err := i.getNumber(xv, trace) if err != nil { return nil, err } - y, err := e.evaluateNumber(yp) + y, err := i.getNumber(yv, trace) if err != nil { return nil, err } - return makeDoubleCheck(e, float64(f(int64(x.value), int64(y.value)))) + return makeDoubleCheck(i, trace, float64(f(int64(x.value), int64(y.value)))) } } @@ -731,64 +669,64 @@ var builtinBitwiseAnd = liftBitwise(func(x, y int64) int64 { return x & y }) var builtinBitwiseOr = liftBitwise(func(x, y int64) int64 { return x | y }) var builtinBitwiseXor = liftBitwise(func(x, y int64) int64 { return x ^ y }) -func builtinObjectFieldsEx(e *evaluator, objp potentialValue, includeHiddenP potentialValue) (value, error) { - obj, err := e.evaluateObject(objp) +func builtinObjectFieldsEx(i *interpreter, trace TraceElement, objv, includeHiddenV value) (value, error) { + obj, err := i.getObject(objv, trace) if err != nil { return nil, err } - includeHidden, err := e.evaluateBoolean(includeHiddenP) + includeHidden, err := i.getBoolean(includeHiddenV, trace) if err != nil { return nil, err } fields := objectFields(obj, withHiddenFromBool(includeHidden.value)) sort.Strings(fields) - elems := []potentialValue{} + elems := []*cachedThunk{} for _, fieldname := range fields { - elems = append(elems, &readyValue{makeValueString(fieldname)}) + elems = append(elems, readyThunk(makeValueString(fieldname))) } return makeValueArray(elems), nil } -func builtinObjectHasEx(e *evaluator, objp potentialValue, fnamep potentialValue, includeHiddenP potentialValue) (value, error) { - obj, err := e.evaluateObject(objp) +func builtinObjectHasEx(i *interpreter, trace TraceElement, objv value, fnamev value, includeHiddenV value) (value, error) { + obj, err := i.getObject(objv, trace) if err != nil { return nil, err } - fname, err := e.evaluateString(fnamep) + fname, err := i.getString(fnamev, trace) if err != nil { return nil, err } - includeHidden, err := e.evaluateBoolean(includeHiddenP) + includeHidden, err := i.getBoolean(includeHiddenV, trace) if err != nil { return nil, err } h := withHiddenFromBool(includeHidden.value) - fieldp := tryObjectIndex(objectBinding(obj), string(fname.value), h) - return makeValueBoolean(fieldp != nil), nil + hasField := objectHasField(objectBinding(obj), string(fname.value), h) + return makeValueBoolean(hasField), nil } -func builtinPow(e *evaluator, basep potentialValue, expp potentialValue) (value, error) { - base, err := e.evaluateNumber(basep) +func builtinPow(i *interpreter, trace TraceElement, basev value, expv value) (value, error) { + base, err := i.getNumber(basev, trace) if err != nil { return nil, err } - exp, err := e.evaluateNumber(expp) + exp, err := i.getNumber(expv, trace) if err != nil { return nil, err } - return makeDoubleCheck(e, math.Pow(base.value, exp.value)) + return makeDoubleCheck(i, trace, math.Pow(base.value, exp.value)) } -func builtinStrReplace(e *evaluator, strp, fromp, top potentialValue) (value, error) { - str, err := e.evaluateString(strp) +func builtinStrReplace(i *interpreter, trace TraceElement, strv, fromv, tov value) (value, error) { + str, err := i.getString(strv, trace) if err != nil { return nil, err } - from, err := e.evaluateString(fromp) + from, err := i.getString(fromv, trace) if err != nil { return nil, err } - to, err := e.evaluateString(top) + to, err := i.getString(tov, trace) if err != nil { return nil, err } @@ -796,13 +734,13 @@ func builtinStrReplace(e *evaluator, strp, fromp, top potentialValue) (value, er sFrom := from.getString() sTo := to.getString() if len(sFrom) == 0 { - return nil, e.Error("'from' string must not be zero length.") + return nil, i.Error("'from' string must not be zero length.", trace) } return makeValueString(strings.Replace(sStr, sFrom, sTo, -1)), nil } -func builtinUglyObjectFlatMerge(e *evaluator, objarrp potentialValue) (value, error) { - objarr, err := e.evaluateArray(objarrp) +func builtinUglyObjectFlatMerge(i *interpreter, trace TraceElement, x value) (value, error) { + objarr, err := i.getArray(x, trace) if err != nil { return nil, err } @@ -811,7 +749,7 @@ func builtinUglyObjectFlatMerge(e *evaluator, objarrp potentialValue) (value, er } newFields := make(simpleObjectFieldMap) for _, elem := range objarr.elements { - obj, err := e.evaluateObject(elem) + obj, err := i.evaluateObject(elem, trace) if err != nil { return nil, err } @@ -819,7 +757,7 @@ func builtinUglyObjectFlatMerge(e *evaluator, objarrp potentialValue) (value, er simpleObj := obj.(*valueSimpleObject) for fieldName, fieldVal := range simpleObj.fields { if _, alreadyExists := newFields[fieldName]; alreadyExists { - return nil, e.Error(duplicateFieldNameErrMsg(fieldName)) + return nil, i.Error(duplicateFieldNameErrMsg(fieldName), trace) } newFields[fieldName] = simpleObjectField{ hide: fieldVal.hide, @@ -837,35 +775,35 @@ func builtinUglyObjectFlatMerge(e *evaluator, objarrp potentialValue) (value, er ), nil } -func builtinExtVar(e *evaluator, namep potentialValue) (value, error) { - name, err := e.evaluateString(namep) +func builtinExtVar(i *interpreter, trace TraceElement, name value) (value, error) { + str, err := i.getString(name, trace) if err != nil { return nil, err } - index := name.getString() - if pv, ok := e.i.extVars[index]; ok { - return e.evaluate(pv) + index := str.getString() + if pv, ok := i.extVars[index]; ok { + return i.evaluatePV(pv, trace) } - return nil, e.Error("Undefined external variable: " + string(index)) + return nil, i.Error("Undefined external variable: "+string(index), trace) } -func builtinNative(e *evaluator, namep potentialValue) (value, error) { - name, err := e.evaluateString(namep) +func builtinNative(i *interpreter, trace TraceElement, name value) (value, error) { + str, err := i.getString(name, trace) if err != nil { return nil, err } - index := name.getString() - if f, exists := e.i.nativeFuncs[index]; exists { + index := str.getString() + if f, exists := i.nativeFuncs[index]; exists { return &valueFunction{ec: f}, nil } - return nil, e.Error(fmt.Sprintf("Unrecognized native function name: %v", index)) + return nil, i.Error(fmt.Sprintf("Unrecognized native function name: %v", index), trace) } -type unaryBuiltinFunc func(*evaluator, potentialValue) (value, error) -type binaryBuiltinFunc func(*evaluator, potentialValue, potentialValue) (value, error) -type ternaryBuiltinFunc func(*evaluator, potentialValue, potentialValue, potentialValue) (value, error) +type unaryBuiltinFunc func(*interpreter, TraceElement, value) (value, error) +type binaryBuiltinFunc func(*interpreter, TraceElement, value, value) (value, error) +type ternaryBuiltinFunc func(*interpreter, TraceElement, value, value, value) (value, error) type unaryBuiltin struct { name ast.Identifier @@ -873,15 +811,19 @@ type unaryBuiltin struct { parameters ast.Identifiers } -func getBuiltinEvaluator(e *evaluator, name ast.Identifier) *evaluator { +func getBuiltinTrace(trace TraceElement, name ast.Identifier) TraceElement { context := "builtin function <" + string(name) + ">" - trace := TraceElement{loc: e.trace.loc, context: &context} - return &evaluator{i: e.i, trace: &trace} + return TraceElement{loc: trace.loc, context: &context} } -func (b *unaryBuiltin) EvalCall(args callArguments, e *evaluator) (value, error) { +func (b *unaryBuiltin) EvalCall(args callArguments, i *interpreter, trace TraceElement) (value, error) { flatArgs := flattenArgs(args, b.Parameters()) - return b.function(getBuiltinEvaluator(e, b.name), flatArgs[0]) + builtinTrace := getBuiltinTrace(trace, b.name) + x, err := flatArgs[0].getValue(i, trace) + if err != nil { + return nil, err + } + return b.function(i, builtinTrace, x) } func (b *unaryBuiltin) Parameters() Parameters { @@ -902,7 +844,7 @@ type binaryBuiltin struct { // It's needed, because it's possible to use named arguments for required parameters. // For example both `toString("x")` and `toString(a="x")` are allowed. // It assumes that we have already checked for duplicates. -func flattenArgs(args callArguments, params Parameters) []potentialValue { +func flattenArgs(args callArguments, params Parameters) []*cachedThunk { if len(args.named) == 0 { return args.positional } @@ -915,7 +857,7 @@ func flattenArgs(args callArguments, params Parameters) []potentialValue { needed[params.required[i]] = i } - flatArgs := make([]potentialValue, len(params.required)) + flatArgs := make([]*cachedThunk, len(params.required)) copy(flatArgs, args.positional) for _, arg := range args.named { flatArgs[needed[arg.name]] = arg.pv @@ -923,9 +865,18 @@ func flattenArgs(args callArguments, params Parameters) []potentialValue { return flatArgs } -func (b *binaryBuiltin) EvalCall(args callArguments, e *evaluator) (value, error) { +func (b *binaryBuiltin) EvalCall(args callArguments, i *interpreter, trace TraceElement) (value, error) { flatArgs := flattenArgs(args, b.Parameters()) - return b.function(getBuiltinEvaluator(e, b.name), flatArgs[0], flatArgs[1]) + builtinTrace := getBuiltinTrace(trace, b.name) + x, err := flatArgs[0].getValue(i, trace) + if err != nil { + return nil, err + } + y, err := flatArgs[1].getValue(i, trace) + if err != nil { + return nil, err + } + return b.function(i, builtinTrace, x, y) } func (b *binaryBuiltin) Parameters() Parameters { @@ -942,9 +893,22 @@ type ternaryBuiltin struct { parameters ast.Identifiers } -func (b *ternaryBuiltin) EvalCall(args callArguments, e *evaluator) (value, error) { +func (b *ternaryBuiltin) EvalCall(args callArguments, i *interpreter, trace TraceElement) (value, error) { flatArgs := flattenArgs(args, b.Parameters()) - return b.function(getBuiltinEvaluator(e, b.name), flatArgs[0], flatArgs[1], flatArgs[2]) + builtinTrace := getBuiltinTrace(trace, b.name) + x, err := flatArgs[0].getValue(i, trace) + if err != nil { + return nil, err + } + y, err := flatArgs[1].getValue(i, trace) + if err != nil { + return nil, err + } + z, err := flatArgs[2].getValue(i, trace) + if err != nil { + return nil, err + } + return b.function(i, builtinTrace, x, y, z) } func (b *ternaryBuiltin) Parameters() Parameters { @@ -982,9 +946,6 @@ var bopBuiltins = []*binaryBuiltin{ ast.BopBitwiseAnd: &binaryBuiltin{name: "operator&", function: builtinBitwiseAnd, parameters: ast.Identifiers{"x", "y"}}, ast.BopBitwiseXor: &binaryBuiltin{name: "operator^", function: builtinBitwiseXor, parameters: ast.Identifiers{"x", "y"}}, ast.BopBitwiseOr: &binaryBuiltin{name: "operator|", function: builtinBitwiseOr, parameters: ast.Identifiers{"x", "y"}}, - - ast.BopAnd: &binaryBuiltin{name: "operator&&", function: builtinAnd, parameters: ast.Identifiers{"x", "y"}}, - ast.BopOr: &binaryBuiltin{name: "operator||", function: builtinOr, parameters: ast.Identifiers{"x", "y"}}, } var uopBuiltins = []*unaryBuiltin{ diff --git a/evaluator.go b/evaluator.go deleted file mode 100644 index f3141a8ab..000000000 --- a/evaluator.go +++ /dev/null @@ -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 -} diff --git a/imports.go b/imports.go index d3be69641..a19add51c 100644 --- a/imports.go +++ b/imports.go @@ -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 diff --git a/interpreter.go b/interpreter.go index c7fadd8c6..6973c8621 100644 --- a/interpreter.go +++ b/interpreter.go @@ -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. @@ -42,23 +42,19 @@ type environment struct { func makeEnvironment(upValues bindingFrame, sb selfBinding) environment { return environment{ - upValues: upValues, - sb: sb, + upValues: upValues, + 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{ - loc: a.Loc(), - context: a.Context(), - }, - i: i, + trace := TraceElement{ + loc: a.Loc(), + context: a.Context(), } 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 err != nil { - return nil, err + if node.Op == ast.BopAnd { + // 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 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) } - return result, nil 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 diff --git a/runtime_error.go b/runtime_error.go index 069203903..6a94745c5 100644 --- a/runtime_error.go +++ b/runtime_error.go @@ -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 diff --git a/testdata/bitwise_and4.golden b/testdata/bitwise_and4.golden index 3769d883c..f8a2a06c3 100644 --- a/testdata/bitwise_and4.golden +++ b/testdata/bitwise_and4.golden @@ -4,11 +4,6 @@ RUNTIME ERROR: x 1 & error "x" -------------------------------------------------- - testdata/bitwise_and4:1:1-14 $ - -1 & error "x" - ------------------------------------------------- During evaluation diff --git a/testdata/bitwise_xor7.golden b/testdata/bitwise_xor7.golden index edcbd1a50..98a4c3112 100644 --- a/testdata/bitwise_xor7.golden +++ b/testdata/bitwise_xor7.golden @@ -4,11 +4,6 @@ RUNTIME ERROR: x 1 ^ error "x" -------------------------------------------------- - testdata/bitwise_xor7:1:1-14 $ - -1 ^ error "x" - ------------------------------------------------- During evaluation diff --git a/testdata/lazy_operator2.golden b/testdata/lazy_operator2.golden index ccbf466ce..2b21d6543 100644 --- a/testdata/lazy_operator2.golden +++ b/testdata/lazy_operator2.golden @@ -4,11 +4,6 @@ RUNTIME ERROR: should happen true && error "should happen" -------------------------------------------------- - testdata/lazy_operator2:1:1-30 $ - -true && error "should happen" - ------------------------------------------------- During evaluation diff --git a/testdata/object_invariant7.golden b/testdata/object_invariant7.golden index e002c968f..c375c8ac1 100644 --- a/testdata/object_invariant7.golden +++ b/testdata/object_invariant7.golden @@ -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 - -{ x: 5, assert super.x == 5 } - ------------------------------------------------- During manifestation diff --git a/testdata/or4.golden b/testdata/or4.golden index fc4cd1785..0c0405aac 100644 --- a/testdata/or4.golden +++ b/testdata/or4.golden @@ -4,11 +4,6 @@ RUNTIME ERROR: xxx false || error "xxx" -------------------------------------------------- - testdata/or4:1:1-21 $ - -false || error "xxx" - ------------------------------------------------- During evaluation diff --git a/testdata/percent_format_str5.golden b/testdata/percent_format_str5.golden index 85e8dc3b3..e9fbefcd4 100644 --- a/testdata/percent_format_str5.golden +++ b/testdata/percent_format_str5.golden @@ -15,12 +15,12 @@ RUNTIME ERROR: Not enough values to format, got 1 std.toString(val) ------------------------------------------------- - :558:9-26 builtin function + :558:9-26 function std.toString(val) ------------------------------------------------- - ... (skipped 14 frames) + ... (skipped 10 frames) ------------------------------------------------- :699:11-64 function diff --git a/testdata/percent_format_str6.golden b/testdata/percent_format_str6.golden index 2769ac359..7a83a4427 100644 --- a/testdata/percent_format_str6.golden +++ b/testdata/percent_format_str6.golden @@ -15,12 +15,12 @@ RUNTIME ERROR: Not enough values to format, got 1 std.toString(val) ------------------------------------------------- - :558:9-26 builtin function + :558:9-26 function std.toString(val) ------------------------------------------------- - ... (skipped 14 frames) + ... (skipped 10 frames) ------------------------------------------------- :699:11-64 function diff --git a/testdata/percent_format_str7.golden b/testdata/percent_format_str7.golden index fbbb330b3..fc1926e96 100644 --- a/testdata/percent_format_str7.golden +++ b/testdata/percent_format_str7.golden @@ -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) ------------------------------------------------- :699:11-64 function diff --git a/testdata/std.filter2.golden b/testdata/std.filter2.golden index ee9dfcde2..e53cee52c 100644 --- a/testdata/std.filter2.golden +++ b/testdata/std.filter2.golden @@ -5,7 +5,7 @@ RUNTIME ERROR: x std.filter(error "x", []) ------------------------------------------------- - testdata/std.filter2:1:1-26 builtin function + testdata/std.filter2:1:1-26 $ std.filter(error "x", []) diff --git a/testdata/std.flatmap5.golden b/testdata/std.flatmap5.golden index e5a77d431..a7d575440 100644 --- a/testdata/std.flatmap5.golden +++ b/testdata/std.flatmap5.golden @@ -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 + testdata/std.flatmap5:2:1-49 $ std.type(std.flatMap(failWith, ["a", "b", "c"])) diff --git a/testdata/std.makeArray_noninteger_big.golden b/testdata/std.makeArray_noninteger_big.golden index 67dd8f217..ec6ea5091 100644 --- a/testdata/std.makeArray_noninteger_big.golden +++ b/testdata/std.makeArray_noninteger_big.golden @@ -1,8 +1,8 @@ RUNTIME ERROR: Expected an integer, but got 1e+100 ------------------------------------------------- - testdata/std.makeArray_noninteger_big:1:1-47 builtin function + testdata/std.makeArray_noninteger_big:1:1-36 builtin function -std.makeArray(1e100, error "shouldn't happen") +std.makeArray(1e100, function(i) i) ------------------------------------------------- During evaluation diff --git a/testdata/std.makeArray_noninteger_big.jsonnet b/testdata/std.makeArray_noninteger_big.jsonnet index 9bd2ba3f7..090fe11a2 100644 --- a/testdata/std.makeArray_noninteger_big.jsonnet +++ b/testdata/std.makeArray_noninteger_big.jsonnet @@ -1 +1 @@ -std.makeArray(1e100, error "shouldn't happen") +std.makeArray(1e100, function(i) i) diff --git a/testdata/std.makeArray_recursive_evalutation_order_matters.golden b/testdata/std.makeArray_recursive_evalutation_order_matters.golden index 976d4d0a5..12994e306 100644 --- a/testdata/std.makeArray_recursive_evalutation_order_matters.golden +++ b/testdata/std.makeArray_recursive_evalutation_order_matters.golden @@ -1,8 +1,6 @@ RUNTIME ERROR: max stack frames exceeded. ------------------------------------------------- - testdata/std.makeArray_recursive_evalutation_order_matters:1:51-61 function - -local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500] + ------------------------------------------------- testdata/std.makeArray_recursive_evalutation_order_matters:1:51-57 function @@ -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 - -local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500] + ------------------------------------------------- testdata/std.makeArray_recursive_evalutation_order_matters:1:51-57 function @@ -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 - -local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500] + ------------------------------------------------- testdata/std.makeArray_recursive_evalutation_order_matters:1:51-57 function @@ -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 - -local arr = [0] + std.makeArray(2000, function(i) arr[i] + 1); arr[500] + ------------------------------------------------- testdata/std.makeArray_recursive_evalutation_order_matters:1:64-72 $ diff --git a/testdata/std.primitiveEquals10.golden b/testdata/std.primitiveEquals10.golden index 8e10a06d0..f93f26640 100644 --- a/testdata/std.primitiveEquals10.golden +++ b/testdata/std.primitiveEquals10.golden @@ -5,7 +5,7 @@ RUNTIME ERROR: x std.primitiveEquals(error "x", 42) ------------------------------------------------- - testdata/std.primitiveEquals10:1:1-35 builtin function + testdata/std.primitiveEquals10:1:1-35 $ std.primitiveEquals(error "x", 42) diff --git a/testdata/std.primitiveEquals9.golden b/testdata/std.primitiveEquals9.golden index 4bc7ae0da..7936e6273 100644 --- a/testdata/std.primitiveEquals9.golden +++ b/testdata/std.primitiveEquals9.golden @@ -5,7 +5,7 @@ RUNTIME ERROR: x std.primitiveEquals(42, error "x") ------------------------------------------------- - testdata/std.primitiveEquals9:1:1-35 builtin function + testdata/std.primitiveEquals9:1:1-35 $ std.primitiveEquals(42, error "x") diff --git a/testdata/std.toString5.golden b/testdata/std.toString5.golden index f393efaca..23c1e35b0 100644 --- a/testdata/std.toString5.golden +++ b/testdata/std.toString5.golden @@ -5,7 +5,7 @@ RUNTIME ERROR: x std.toString(error "x") ------------------------------------------------- - testdata/std.toString5:1:1-24 builtin function + testdata/std.toString5:1:1-24 $ std.toString(error "x") diff --git a/testdata/type_error.golden b/testdata/type_error.golden index a002513d6..7f09d6d2a 100644 --- a/testdata/type_error.golden +++ b/testdata/type_error.golden @@ -5,7 +5,7 @@ RUNTIME ERROR: xxx std.type(error "xxx") ------------------------------------------------- - testdata/type_error:1:1-22 builtin function + testdata/type_error:1:1-22 $ std.type(error "xxx") diff --git a/thunks.go b/thunks.go index 5cfab6aa7..da0d2cdea 100644 --- a/thunks.go +++ b/thunks.go @@ -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 + } + if !objectHasField(sb.super(), fieldName, withHidden) { + return right, nil + } + left, err := objectIndex(i, trace, sb.super(), fieldName) + if err != nil { + return nil, err } - return right + 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 := ¶meters.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 { @@ -268,10 +213,8 @@ func prepareClosureParameters(parameters ast.Parameters, env environment) Parame optionalParameters := make([]namedParameter, 0, len(parameters.Optional)) for _, named := range parameters.Optional { optionalParameters = append(optionalParameters, namedParameter{ - name: named.Name, - defaultArg: &defaultArgument{ - body: named.DefaultArg, - }, + name: named.Name, + 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} } diff --git a/value.go b/value.go index 07bca8a90..71cf4ad55 100644 --- a/value.go +++ b/value.go @@ -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(args callArguments) potentialValue { - return makeCallThunk(f.ec, args) +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 + } + 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 nil, i.Error("Attempt to use super when there is no super class.", trace) } - return e.evaluate(objp) -} -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