Support for native callbacks

This commit is contained in:
Stanisław Barzowski 2017-10-09 12:53:34 -04:00 committed by Dave Cunningham
parent 96a2abc46c
commit ed281bc563
18 changed files with 181 additions and 22 deletions

View File

@ -589,13 +589,27 @@ func builtinExtVar(e *evaluator, namep potentialValue) (value, error) {
if err != nil {
return nil, err
}
index := ast.Identifier(name.getString())
index := name.getString()
if pv, ok := e.i.extVars[index]; ok {
return e.evaluate(pv)
}
return nil, e.Error("Undefined external variable: " + string(index))
}
func builtinNative(e *evaluator, namep potentialValue) (value, error) {
name, err := e.evaluateString(namep)
if err != nil {
return nil, err
}
index := name.getString()
if f, exists := e.i.nativeFuncs[index]; exists {
return &valueFunction{ec: f}, nil
}
return nil, e.Error(fmt.Sprintf("Unrecognized native function name: %v", index))
}
type unaryBuiltin func(*evaluator, potentialValue) (value, error)
type binaryBuiltin func(*evaluator, potentialValue, potentialValue) (value, error)
type ternaryBuiltin func(*evaluator, potentialValue, potentialValue, potentialValue) (value, error)
@ -772,6 +786,7 @@ var funcBuiltins = buildBuiltinMap([]builtin{
&BinaryBuiltin{name: "pow", function: builtinPow, parameters: ast.Identifiers{"base", "exp"}},
&BinaryBuiltin{name: "modulo", function: builtinModulo, parameters: ast.Identifiers{"x", "y"}},
&UnaryBuiltin{name: "md5", function: builtinMd5, parameters: ast.Identifiers{"x"}},
&UnaryBuiltin{name: "native", function: builtinNative, parameters: ast.Identifiers{"x"}},
// internal
&UnaryBuiltin{name: "$objectFlatMerge", function: builtinUglyObjectFlatMerge, parameters: ast.Identifiers{"x"}},

View File

@ -298,6 +298,12 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) {
return
}
}
for i := range node.Arguments.Named {
err = desugar(&node.Arguments.Named[i].Arg, objLevel)
if err != nil {
return
}
}
case *ast.ApplyBrace:
err = desugar(&node.Left, objLevel)

View File

@ -214,7 +214,10 @@ type interpreter struct {
stack callStack
// External variables
extVars map[ast.Identifier]potentialValue
extVars map[string]potentialValue
// Native functions
nativeFuncs map[string]*nativeFunction
// A part of std object common to all files
baseStd valueObject
@ -730,6 +733,46 @@ func (i *interpreter) manifestAndSerializeJSON(trace *TraceElement, v value, mul
return buf.String(), nil
}
func jsonToValue(e *evaluator, 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)
if err != nil {
return nil, err
}
elems[i] = &readyValue{val}
}
return makeValueArray(elems), nil
case bool:
return makeValueBoolean(v), nil
case float64:
return makeValueNumber(v), nil
case map[string]interface{}:
fieldMap := map[string]value{}
for name, f := range v {
val, err := jsonToValue(e, f)
if err != nil {
return nil, err
}
fieldMap[name] = val
}
return buildObject(ast.ObjectFieldInherit, fieldMap), nil
case string:
return makeValueString(v), nil
default:
return nil, e.Error(fmt.Sprintf("Not a json type: %#+v", v))
}
}
func (i *interpreter) EvalInCleanEnv(fromWhere *TraceElement, env *environment, ast ast.Node, trimmable bool) (value, error) {
err := i.newCall(fromWhere, *env, trimmable)
if err != nil {
@ -776,8 +819,8 @@ func evaluateStd(i *interpreter) (value, error) {
return i.EvalInCleanEnv(evalTrace, &beforeStdEnv, node, false)
}
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[ast.Identifier]potentialValue {
result := make(map[ast.Identifier]potentialValue)
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[string]potentialValue {
result := make(map[string]potentialValue)
for name, content := range ext {
if content.isCode {
varLoc := ast.MakeLocationRangeMessage("During evaluation")
@ -788,9 +831,9 @@ func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[ast.Identifie
i: i,
trace: varTrace,
}
result[ast.Identifier(name)] = codeToPV(e, "<"+kind+":"+name+">", content.value)
result[name] = codeToPV(e, "<"+kind+":"+name+">", content.value)
} else {
result[ast.Identifier(name)] = &readyValue{makeValueString(content.value)}
result[name] = &readyValue{makeValueString(content.value)}
}
}
return result
@ -804,10 +847,11 @@ func buildObject(hide ast.ObjectFieldHide, fields map[string]value) valueObject
return makeValueSimpleObject(bindingFrame{}, fieldMap, nil)
}
func buildInterpreter(ext vmExtMap, maxStack int, importer Importer) (*interpreter, error) {
func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*nativeFunction, maxStack int, importer Importer) (*interpreter, error) {
i := interpreter{
stack: makeCallStack(maxStack),
importCache: MakeImportCache(importer),
nativeFuncs: nativeFuncs,
}
stdObj, err := buildStdObject(&i)
@ -834,8 +878,9 @@ func makeInitialEnv(filename string, baseStd valueObject) environment {
)
}
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, maxStack int, importer Importer) (string, error) {
i, err := buildInterpreter(ext, maxStack, importer)
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*nativeFunction, maxStack int, importer Importer) (string, error) {
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
if err != nil {
return "", err
}
@ -854,7 +899,7 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, maxStack int, importer
toplevelArgMap := prepareExtVars(i, tla, "top-level-arg")
args := callArguments{}
for argName, pv := range toplevelArgMap {
args.named = append(args.named, namedCallArgument{name: argName, pv: pv})
args.named = append(args.named, namedCallArgument{name: ast.Identifier(argName), pv: pv})
}
funcLoc := ast.MakeLocationRangeMessage("Top-level-function")
funcTrace := &TraceElement{

View File

@ -18,6 +18,7 @@ package jsonnet
import (
"bytes"
"encoding/json"
"flag"
"io/ioutil"
"path/filepath"
@ -25,6 +26,7 @@ import (
"testing"
"unicode/utf8"
"github.com/google/go-jsonnet/ast"
"github.com/sergi/go-diff/diffmatchpatch"
)
@ -100,6 +102,17 @@ func TestMain(t *testing.T) {
vm.ExtCode(name, value)
}
vm.NativeFunction(&nativeFunction{
name: "jsonToString",
params: ast.Identifiers{"x"},
f: func(x []interface{}) (interface{}, error) {
bytes, err := json.Marshal(x[0])
if err != nil {
return nil, err
}
return string(bytes), nil
},
})
read := func(file string) []byte {
bytz, err := ioutil.ReadFile(file)
if err != nil {

1
testdata/native1.golden vendored Normal file
View File

@ -0,0 +1 @@
"\"test\""

1
testdata/native1.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
std.native("jsonToString")("test")

1
testdata/native2.golden vendored Normal file
View File

@ -0,0 +1 @@
"{}"

1
testdata/native2.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
std.native("jsonToString")({})

1
testdata/native3.golden vendored Normal file
View File

@ -0,0 +1 @@
"{\"x\":{\"y\":\"z\"},\"xx\":42}"

1
testdata/native3.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
std.native("jsonToString")({"x": {"y": "z"}, "xx": 42})

15
testdata/native4.golden vendored Normal file
View File

@ -0,0 +1,15 @@
RUNTIME ERROR: xxx
-------------------------------------------------
testdata/native4:1:28-38 thunk from <$>
std.native("jsonToString")(error "xxx")
-------------------------------------------------
testdata/native4:1:1-40 $
std.native("jsonToString")(error "xxx")
-------------------------------------------------
During evaluation

1
testdata/native4.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
std.native("jsonToString")(error "xxx")

10
testdata/native5.golden vendored Normal file
View File

@ -0,0 +1,10 @@
RUNTIME ERROR: Couldn't manifest function in JSON output.
-------------------------------------------------
testdata/native5:1:1-42 $
std.native("jsonToString")(function() 42)
-------------------------------------------------
During evaluation

1
testdata/native5.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
std.native("jsonToString")(function() 42)

8
testdata/native_nonexistent.golden vendored Normal file
View File

@ -0,0 +1,8 @@
RUNTIME ERROR: Unrecognized native function name: blah
-------------------------------------------------
<builtin> builtin function <native>
-------------------------------------------------
During evaluation

1
testdata/native_nonexistent.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
std.native("blah")

View File

@ -287,6 +287,37 @@ func makeClosure(env environment, function *ast.Function) *closure {
}
}
type nativeFunction struct {
f func([]interface{}) (interface{}, error)
params ast.Identifiers
name string
}
func (native *nativeFunction) EvalCall(arguments callArguments, e *evaluator) (value, error) {
flatArgs := flattenArgs(arguments, native.Parameters())
nativeArgs := make([]interface{}, 0, len(flatArgs))
for _, arg := range flatArgs {
v, err := e.evaluate(arg)
if err != nil {
return nil, err
}
json, err := e.i.manifestJSON(e.trace, v)
if err != nil {
return nil, err
}
nativeArgs = append(nativeArgs, json)
}
resultJSON, err := native.f(nativeArgs)
if err != nil {
return nil, err
}
return jsonToValue(e, resultJSON)
}
func (native *nativeFunction) Parameters() Parameters {
return Parameters{required: native.params}
}
// partialPotentialValue
// -------------------------------------

9
vm.go
View File

@ -35,6 +35,7 @@ type VM struct {
MaxTrace int // The number of lines of stack trace to display (0 for all of them).
ext vmExtMap
tla vmExtMap
nativeFuncs map[string]*nativeFunction
importer Importer
ef ErrorFormatter
}
@ -56,6 +57,7 @@ func MakeVM() *VM {
MaxStack: 500,
ext: make(vmExtMap),
tla: make(vmExtMap),
nativeFuncs: make(map[string]*nativeFunction),
ef: ErrorFormatter{pretty: true, colorful: true, MaxStackTraceSize: 20},
importer: &FileImporter{},
}
@ -96,13 +98,18 @@ func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, e
if err != nil {
return "", err
}
output, err = evaluate(node, vm.ext, vm.tla, vm.MaxStack, vm.importer)
output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer)
if err != nil {
return "", err
}
return output, nil
}
// NativeFunction registers a native function
func (vm *VM) NativeFunction(f *nativeFunction) {
vm.nativeFuncs[f.name] = f
}
// EvaluateSnippet evaluates a string containing Jsonnet code, return a JSON
// string.
//