diff --git a/.gitignore b/.gitignore index a1a205147..16204896a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ coverage.out /tests_path.source /jsonnet/jsonnet *.so +*.prof diff --git a/ast/ast.go b/ast/ast.go index 7231a79ca..1e9f389f3 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -322,8 +322,8 @@ type NamedParameter struct { } type Parameters struct { - Positional Identifiers - Named []NamedParameter + Required Identifiers + Optional []NamedParameter } // --------------------------------------------------------------------------- diff --git a/builtins.go b/builtins.go index 9873948f3..1f30a8dc5 100644 --- a/builtins.go +++ b/builtins.go @@ -224,7 +224,7 @@ func builtinLength(e *evaluator, xp potentialValue) (value, error) { case *valueString: num = x.length() case *valueFunction: - num = len(x.parameters()) + num = len(x.parameters().required) default: return nil, e.typeErrorGeneral(x) } @@ -614,13 +614,12 @@ func getBuiltinEvaluator(e *evaluator, name ast.Identifier) *evaluator { } func (b *UnaryBuiltin) EvalCall(args callArguments, e *evaluator) (value, error) { - - // TODO check args - return b.function(getBuiltinEvaluator(e, b.name), args.positional[0]) + flatArgs := flattenArgs(args, b.Parameters()) + return b.function(getBuiltinEvaluator(e, b.name), flatArgs[0]) } -func (b *UnaryBuiltin) Parameters() ast.Identifiers { - return b.parameters +func (b *UnaryBuiltin) Parameters() Parameters { + return Parameters{required: b.parameters} } type BinaryBuiltin struct { @@ -629,13 +628,38 @@ type BinaryBuiltin struct { parameters ast.Identifiers } +// flattenArgs transforms all arguments to a simple array of positional arguments. +// 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 { + if len(args.named) == 0 { + return args.positional + } + if len(params.optional) != 0 { + panic("Can't normalize arguments if optional parameters are present") + } + needed := make(map[ast.Identifier]int) + + for i := len(args.positional); i < len(params.required); i++ { + needed[params.required[i]] = i + } + + flatArgs := make([]potentialValue, len(params.required)) + copy(flatArgs, args.positional) + for _, arg := range args.named { + flatArgs[needed[arg.name]] = arg.pv + } + return flatArgs +} + func (b *BinaryBuiltin) EvalCall(args callArguments, e *evaluator) (value, error) { - // TODO check args - return b.function(getBuiltinEvaluator(e, b.name), args.positional[0], args.positional[1]) + flatArgs := flattenArgs(args, b.Parameters()) + return b.function(getBuiltinEvaluator(e, b.name), flatArgs[0], flatArgs[1]) } -func (b *BinaryBuiltin) Parameters() ast.Identifiers { - return b.parameters +func (b *BinaryBuiltin) Parameters() Parameters { + return Parameters{required: b.parameters} } type TernaryBuiltin struct { @@ -645,12 +669,12 @@ type TernaryBuiltin struct { } func (b *TernaryBuiltin) EvalCall(args callArguments, e *evaluator) (value, error) { - // TODO check args - return b.function(getBuiltinEvaluator(e, b.name), args.positional[0], args.positional[1], args.positional[2]) + flatArgs := flattenArgs(args, b.Parameters()) + return b.function(getBuiltinEvaluator(e, b.name), flatArgs[0], flatArgs[1], flatArgs[2]) } -func (b *TernaryBuiltin) Parameters() ast.Identifiers { - return b.parameters +func (b *TernaryBuiltin) Parameters() Parameters { + return Parameters{required: b.parameters} } var desugaredBop = map[ast.BinaryOp]ast.Identifier{ @@ -698,7 +722,7 @@ var uopBuiltins = []*UnaryBuiltin{ var funcBuiltins = map[string]evalCallable{ "extVar": &UnaryBuiltin{name: "extVar", function: builtinExtVar, parameters: ast.Identifiers{"x"}}, "length": &UnaryBuiltin{name: "length", function: builtinLength, parameters: ast.Identifiers{"x"}}, - "toString": &UnaryBuiltin{name: "toString", function: builtinToString, parameters: ast.Identifiers{"x"}}, + "toString": &UnaryBuiltin{name: "toString", function: builtinToString, parameters: ast.Identifiers{"a"}}, "makeArray": &BinaryBuiltin{name: "makeArray", function: builtinMakeArray, parameters: ast.Identifiers{"sz", "func"}}, "flatMap": &BinaryBuiltin{name: "flatMap", function: builtinFlatMap, parameters: ast.Identifiers{"func", "arr"}}, "filter": &BinaryBuiltin{name: "filter", function: builtinFilter, parameters: ast.Identifiers{"func", "arr"}}, diff --git a/desugarer.go b/desugarer.go index 148b4a29b..0eeb45e08 100644 --- a/desugarer.go +++ b/desugarer.go @@ -86,28 +86,6 @@ func stringUnescape(loc *ast.LocationRange, s string) (string, error) { } func desugarFields(location ast.LocationRange, fields *ast.ObjectFields, objLevel int) error { - - // Desugar children - for i := range *fields { - field := &((*fields)[i]) - if field.Expr1 != nil { - err := desugar(&field.Expr1, objLevel) - if err != nil { - return err - } - } - err := desugar(&field.Expr2, objLevel+1) - if err != nil { - return err - } - if field.Expr3 != nil { - err := desugar(&field.Expr3, objLevel+1) - if err != nil { - return err - } - } - } - // Simplify asserts for i := range *fields { field := &(*fields)[i] @@ -187,7 +165,7 @@ func desugarFields(location ast.LocationRange, fields *ast.ObjectFields, objLeve func simpleLambda(body ast.Node, paramName ast.Identifier) ast.Node { return &ast.Function{ Body: body, - Parameters: ast.Parameters{Positional: ast.Identifiers{paramName}}, + Parameters: ast.Parameters{Required: ast.Identifiers{paramName}}, } } @@ -233,8 +211,6 @@ func desugarObjectComp(comp *ast.ObjectComp, objLevel int) (ast.Node, error) { comp.Fields = append(comp.Fields, ast.ObjectFieldLocalNoMethod(&dollar, &ast.Self{})) } - // TODO(sbarzowski) find a consistent convention to prevent desugaring the same thing twice - // here we deeply desugar fields and it will happen again err := desugarFields(*comp.Loc(), &comp.Fields, objLevel+1) if err != nil { return nil, err @@ -383,6 +359,7 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) { Expr: buildStdCall(desugaredBop[ast.BopManifestEqual], node.Left, node.Right), } } else if node.Op == ast.BopIn { + // reversed order of arguments *astPtr = buildStdCall(funcname, node.Right, node.Left) } else { *astPtr = buildStdCall(funcname, node.Left, node.Right) @@ -429,6 +406,13 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) { } case *ast.Function: + for i := range node.Parameters.Optional { + param := &node.Parameters.Optional[i] + err = desugar(¶m.DefaultArg, objLevel) + if err != nil { + return + } + } err = desugar(&node.Body, objLevel) if err != nil { return @@ -476,7 +460,10 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) { node.Step = &ast.LiteralNull{} } *astPtr = buildStdCall("slice", node.Target, node.BeginIndex, node.EndIndex, node.Step) - desugar(astPtr, objLevel) + err = desugar(astPtr, objLevel) + if err != nil { + return + } case *ast.Local: for i := range node.Binds { @@ -529,9 +516,32 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) { } *astPtr = buildDesugaredObject(node.NodeBase, node.Fields) + err = desugar(astPtr, objLevel) + if err != nil { + return + } case *ast.DesugaredObject: - return nil + for i := range node.Fields { + field := &((node.Fields)[i]) + if field.Name != nil { + err := desugar(&field.Name, objLevel) + if err != nil { + return err + } + } + err := desugar(&field.Body, objLevel+1) + if err != nil { + return err + } + } + for i := range node.Asserts { + assert := &((node.Asserts)[i]) + err := desugar(assert, objLevel+1) + if err != nil { + return err + } + } case *ast.ObjectComp: comp, err := desugarObjectComp(node, objLevel) diff --git a/interpreter.go b/interpreter.go index 05837c649..a7b0d21fd 100644 --- a/interpreter.go +++ b/interpreter.go @@ -464,11 +464,16 @@ func (i *interpreter) evaluate(a ast.Node) (value, error) { arguments := callArguments{ positional: make([]potentialValue, len(ast.Arguments.Positional)), + named: make([]namedCallArgument, len(ast.Arguments.Named)), } for i, arg := range ast.Arguments.Positional { arguments.positional[i] = makeThunk(argEnv, arg) } + for i, arg := range ast.Arguments.Named { + arguments.named[i] = namedCallArgument{name: arg.Name, pv: makeThunk(argEnv, arg.Arg)} + } + return e.evaluate(function.call(arguments)) default: diff --git a/main_test.go b/main_test.go index 3f171e017..63d8cdccf 100644 --- a/main_test.go +++ b/main_test.go @@ -23,7 +23,6 @@ import ( "path/filepath" "strings" "testing" - "unicode/utf8" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -40,25 +39,6 @@ type mainTest struct { golden string } -func removeExcessiveWhitespace(s string) string { - var buf bytes.Buffer - separated := true - for i, w := 0, 0; i < len(s); i += w { - runeValue, width := utf8.DecodeRuneInString(s[i:]) - if runeValue == '\n' || runeValue == ' ' { - if !separated { - buf.WriteString(" ") - separated = true - } - } else { - buf.WriteRune(runeValue) - separated = false - } - w = width - } - return buf.String() -} - func setExtVars(vm *VM) { // TODO(sbarzowski) extract, so that it's possible to define extvars per-test // Check that it doesn't get evaluated. diff --git a/mutually_recursive_defaults.input b/mutually_recursive_defaults.input new file mode 100644 index 000000000..4cb1bf648 --- /dev/null +++ b/mutually_recursive_defaults.input @@ -0,0 +1 @@ +(function(a=[1, b[1]], b=[a[0], 2]) [a, b])() diff --git a/parser/context.go b/parser/context.go index 4bff536f2..967a41bc9 100644 --- a/parser/context.go +++ b/parser/context.go @@ -313,9 +313,9 @@ func addContext(node ast.Node, context *string, bind string) { case *ast.Function: funContext := functionContext(bind) addContext(node.Body, funContext, anonymous) - for i := range node.Parameters.Named { + for i := range node.Parameters.Optional { // Default arguments have the same context as the function body. - addContext(node.Parameters.Named[i].DefaultArg, funContext, anonymous) + addContext(node.Parameters.Optional[i].DefaultArg, funContext, anonymous) } case *ast.Object: // TODO(sbarzowski) include fieldname, maybe even chains diff --git a/parser/parser.go b/parser/parser.go index 78dbc40f4..538d9d30d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -200,10 +200,10 @@ func (p *parser) parseParameters(elementKind string) (*ast.Parameters, bool, err if !ok { return nil, false, MakeStaticError(fmt.Sprintf("Expected simple identifier but got a complex expression."), *arg.Loc()) } - params.Positional = append(params.Positional, *id) + params.Required = append(params.Required, *id) } for _, arg := range args.Named { - params.Named = append(params.Named, ast.NamedParameter{Name: arg.Name, DefaultArg: arg.Arg}) + params.Optional = append(params.Optional, ast.NamedParameter{Name: arg.Name, DefaultArg: arg.Arg}) } return ¶ms, trailingComma, nil } diff --git a/static_analyzer.go b/static_analyzer.go index 56ab9f857..e64268a7d 100644 --- a/static_analyzer.go +++ b/static_analyzer.go @@ -46,6 +46,9 @@ func analyzeVisit(a ast.Node, inObject bool, vars ast.IdentifierSet) error { for _, arg := range a.Arguments.Positional { visitNext(arg, inObject, vars, s) } + for _, arg := range a.Arguments.Named { + visitNext(arg.Arg, inObject, vars, s) + } case *ast.Array: for _, elem := range a.Elements { visitNext(elem, inObject, vars, s) @@ -60,18 +63,24 @@ func analyzeVisit(a ast.Node, inObject bool, vars ast.IdentifierSet) error { case *ast.Error: visitNext(a.Expr, inObject, vars, s) case *ast.Function: - // TODO(sbarzowski) check duplicate function parameters - // or maybe somewhere else as it doesn't require any context newVars := vars.Clone() - for _, param := range a.Parameters.Positional { + for _, param := range a.Parameters.Required { newVars.Add(param) } + for _, param := range a.Parameters.Optional { + newVars.Add(param.Name) + } + for _, param := range a.Parameters.Optional { + visitNext(param.DefaultArg, inObject, newVars, s) + } visitNext(a.Body, inObject, newVars, s) // Parameters are free inside the body, but not visible here or outside - for _, param := range a.Parameters.Positional { + for _, param := range a.Parameters.Required { s.freeVars.Remove(param) } - // TODO(sbarzowski) when we have default values of params check them + for _, param := range a.Parameters.Optional { + s.freeVars.Remove(param.Name) + } case *ast.Import: //nothing to do here case *ast.ImportStr: diff --git a/std.thisFile.jsonnet b/std.thisFile.jsonnet new file mode 100644 index 000000000..f413f6e22 --- /dev/null +++ b/std.thisFile.jsonnet @@ -0,0 +1 @@ +std.thisFile diff --git a/testdata/bad_function_call.golden b/testdata/bad_function_call.golden index 4aaa7d32f..3d890cd75 100644 --- a/testdata/bad_function_call.golden +++ b/testdata/bad_function_call.golden @@ -1,4 +1,4 @@ -RUNTIME ERROR: function expected 1 argument(s), but got 0 +RUNTIME ERROR: Missing argument: x ------------------------------------------------- testdata/bad_function_call:1:1-18 $ diff --git a/testdata/bad_function_call2.golden b/testdata/bad_function_call2.golden index 3af428bed..1cf894cf9 100644 --- a/testdata/bad_function_call2.golden +++ b/testdata/bad_function_call2.golden @@ -1,4 +1,4 @@ -RUNTIME ERROR: function expected 1 argument(s), but got 2 +RUNTIME ERROR: Function expected 1 positional argument(s), but got 2 ------------------------------------------------- testdata/bad_function_call2:1:1-22 $ diff --git a/testdata/bad_function_call_and_error.golden b/testdata/bad_function_call_and_error.golden index 404490a66..39d26557d 100644 --- a/testdata/bad_function_call_and_error.golden +++ b/testdata/bad_function_call_and_error.golden @@ -1,4 +1,4 @@ -RUNTIME ERROR: function expected 1 argument(s), but got 2 +RUNTIME ERROR: Function expected 1 positional argument(s), but got 2 ------------------------------------------------- testdata/bad_function_call_and_error:1:1-38 $ diff --git a/testdata/optional_args.golden b/testdata/optional_args.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args.jsonnet b/testdata/optional_args.jsonnet new file mode 100644 index 000000000..2becdb71e --- /dev/null +++ b/testdata/optional_args.jsonnet @@ -0,0 +1 @@ +local foo(x=42) = x; foo() diff --git a/testdata/optional_args10.golden b/testdata/optional_args10.golden new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/testdata/optional_args10.golden @@ -0,0 +1 @@ +2 diff --git a/testdata/optional_args10.jsonnet b/testdata/optional_args10.jsonnet new file mode 100644 index 000000000..98cd4fbde --- /dev/null +++ b/testdata/optional_args10.jsonnet @@ -0,0 +1 @@ +(function(x, y) x)(y=1, x=2) diff --git a/testdata/optional_args11.golden b/testdata/optional_args11.golden new file mode 100644 index 000000000..d86a1a4af --- /dev/null +++ b/testdata/optional_args11.golden @@ -0,0 +1,10 @@ +RUNTIME ERROR: Argument x already provided +------------------------------------------------- + testdata/optional_args11:1:1-30 $ + +(function(x, y) 42)(42, x=42) + +------------------------------------------------- + During evaluation + + diff --git a/testdata/optional_args11.jsonnet b/testdata/optional_args11.jsonnet new file mode 100644 index 000000000..f5de182d9 --- /dev/null +++ b/testdata/optional_args11.jsonnet @@ -0,0 +1 @@ +(function(x, y) 42)(42, x=42) diff --git a/testdata/optional_args12.golden b/testdata/optional_args12.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args12.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args12.jsonnet b/testdata/optional_args12.jsonnet new file mode 100644 index 000000000..1f576baf2 --- /dev/null +++ b/testdata/optional_args12.jsonnet @@ -0,0 +1 @@ +(function(x=42, y=42) 42)(y=42) diff --git a/testdata/optional_args13.golden b/testdata/optional_args13.golden new file mode 100644 index 000000000..60d42d443 --- /dev/null +++ b/testdata/optional_args13.golden @@ -0,0 +1,10 @@ +RUNTIME ERROR: Missing argument: y +------------------------------------------------- + testdata/optional_args13:1:1-26 $ + +(function(x, y) 42)(x=42) + +------------------------------------------------- + During evaluation + + diff --git a/testdata/optional_args13.jsonnet b/testdata/optional_args13.jsonnet new file mode 100644 index 000000000..0a08516b0 --- /dev/null +++ b/testdata/optional_args13.jsonnet @@ -0,0 +1 @@ +(function(x, y) 42)(x=42) diff --git a/testdata/optional_args14.golden b/testdata/optional_args14.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args14.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args14.jsonnet b/testdata/optional_args14.jsonnet new file mode 100644 index 000000000..ed03b9d2f --- /dev/null +++ b/testdata/optional_args14.jsonnet @@ -0,0 +1 @@ +(function(a=b, b=a) 42)() diff --git a/testdata/optional_args15.golden b/testdata/optional_args15.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args15.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args15.jsonnet b/testdata/optional_args15.jsonnet new file mode 100644 index 000000000..e5d071d0f --- /dev/null +++ b/testdata/optional_args15.jsonnet @@ -0,0 +1 @@ +{ g: 42, f(a=self.g): a }.f() diff --git a/testdata/optional_args16.golden b/testdata/optional_args16.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args16.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args16.jsonnet b/testdata/optional_args16.jsonnet new file mode 100644 index 000000000..88c02a2b1 --- /dev/null +++ b/testdata/optional_args16.jsonnet @@ -0,0 +1 @@ +(function(x=17) x)(42) diff --git a/testdata/optional_args17.golden b/testdata/optional_args17.golden new file mode 100644 index 000000000..00750edc0 --- /dev/null +++ b/testdata/optional_args17.golden @@ -0,0 +1 @@ +3 diff --git a/testdata/optional_args17.jsonnet b/testdata/optional_args17.jsonnet new file mode 100644 index 000000000..403afd39c --- /dev/null +++ b/testdata/optional_args17.jsonnet @@ -0,0 +1 @@ +(function(x, y=42) x + y)(y=1, x=2) diff --git a/testdata/optional_args18.golden b/testdata/optional_args18.golden new file mode 100644 index 000000000..901bb0f6e --- /dev/null +++ b/testdata/optional_args18.golden @@ -0,0 +1,6 @@ +[ + 42, + 43, + 44, + 45 +] diff --git a/testdata/optional_args18.jsonnet b/testdata/optional_args18.jsonnet new file mode 100644 index 000000000..938a1478b --- /dev/null +++ b/testdata/optional_args18.jsonnet @@ -0,0 +1 @@ +(function(a1=1, a2=a1+1, a3=a2+1, a4=a3+1) [a1, a2, a3, a4])(a1=42) diff --git a/testdata/optional_args19.golden b/testdata/optional_args19.golden new file mode 100644 index 000000000..ffdd522cd --- /dev/null +++ b/testdata/optional_args19.golden @@ -0,0 +1,6 @@ +[ + 45, + 44, + 43, + 42 +] diff --git a/testdata/optional_args19.jsonnet b/testdata/optional_args19.jsonnet new file mode 100644 index 000000000..88aab3e28 --- /dev/null +++ b/testdata/optional_args19.jsonnet @@ -0,0 +1 @@ +(function(a1=a2+1, a2=a3+1, a3=a4+1, a4=1) [a1, a2, a3, a4])(a4=42) diff --git a/testdata/optional_args2.golden b/testdata/optional_args2.golden new file mode 100644 index 000000000..6fd4417ac --- /dev/null +++ b/testdata/optional_args2.golden @@ -0,0 +1,5 @@ +{ + "x": 2, + "y": 4, + "z": 2 +} diff --git a/testdata/optional_args2.jsonnet b/testdata/optional_args2.jsonnet new file mode 100644 index 000000000..0da31fce3 --- /dev/null +++ b/testdata/optional_args2.jsonnet @@ -0,0 +1,4 @@ +local x = 1; +local foo(x=2, y=3, z=x) = {x: x, y: y, z: z}; +local x = 4; +foo(y=x) diff --git a/testdata/optional_args20.golden b/testdata/optional_args20.golden new file mode 100644 index 000000000..b1178090a --- /dev/null +++ b/testdata/optional_args20.golden @@ -0,0 +1,18 @@ +[ + [ + 42, + 49 + ], + [ + 43, + 48 + ], + [ + 44, + 47 + ], + [ + 45, + 46 + ] +] diff --git a/testdata/optional_args20.jsonnet b/testdata/optional_args20.jsonnet new file mode 100644 index 000000000..6395976d9 --- /dev/null +++ b/testdata/optional_args20.jsonnet @@ -0,0 +1 @@ +(function(x, a1=[x, a2[1] + 1], a2=[a1[0] + 1, a3[1] + 1], a3=[a2[0] + 1, a4[1] + 1], a4=[a3[0] + 1, a4[0] + 1]) [a1, a2, a3, a4])(x=42) diff --git a/testdata/optional_args21.golden b/testdata/optional_args21.golden new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/testdata/optional_args21.golden @@ -0,0 +1 @@ +2 diff --git a/testdata/optional_args21.jsonnet b/testdata/optional_args21.jsonnet new file mode 100644 index 000000000..dcf0b0f72 --- /dev/null +++ b/testdata/optional_args21.jsonnet @@ -0,0 +1,2 @@ +local foo(x=2, z=x) = x; +foo() diff --git a/testdata/optional_args22.golden b/testdata/optional_args22.golden new file mode 100644 index 000000000..49c115091 --- /dev/null +++ b/testdata/optional_args22.golden @@ -0,0 +1,10 @@ +[ + [ + 42, + 42 + ], + [ + 42, + 17 + ] +] diff --git a/testdata/optional_args22.jsonnet b/testdata/optional_args22.jsonnet new file mode 100644 index 000000000..62345e5e8 --- /dev/null +++ b/testdata/optional_args22.jsonnet @@ -0,0 +1,2 @@ +local foo(x) = local bar(y=x) = [x, y]; bar; +[foo(42)(), foo(42)(17)] diff --git a/testdata/optional_args3.golden b/testdata/optional_args3.golden new file mode 100644 index 000000000..f8d884c74 --- /dev/null +++ b/testdata/optional_args3.golden @@ -0,0 +1,5 @@ +{ + "x": 2, + "y": 1, + "z": 2 +} diff --git a/testdata/optional_args3.jsonnet b/testdata/optional_args3.jsonnet new file mode 100644 index 000000000..ee63cff74 --- /dev/null +++ b/testdata/optional_args3.jsonnet @@ -0,0 +1,3 @@ +local x = 1; +local foo(x=2, y=3, z=x) = {x: x, y: y, z: z}; +foo(y=x) diff --git a/testdata/optional_args4.golden b/testdata/optional_args4.golden new file mode 100644 index 000000000..5c1279961 --- /dev/null +++ b/testdata/optional_args4.golden @@ -0,0 +1,5 @@ +{ + "x": 5, + "y": 4, + "z": 5 +} diff --git a/testdata/optional_args4.jsonnet b/testdata/optional_args4.jsonnet new file mode 100644 index 000000000..12f544089 --- /dev/null +++ b/testdata/optional_args4.jsonnet @@ -0,0 +1,4 @@ +local x = 1; +local foo(x=2, y=3, z=x) = {x: x, y: y, z: z}; +local x = 4; +foo(x=5, y=x) diff --git a/testdata/optional_args5.golden b/testdata/optional_args5.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args5.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args5.jsonnet b/testdata/optional_args5.jsonnet new file mode 100644 index 000000000..f69f35a6a --- /dev/null +++ b/testdata/optional_args5.jsonnet @@ -0,0 +1,2 @@ +local foo(x, y=x) = y; +foo(42) diff --git a/testdata/optional_args6.golden b/testdata/optional_args6.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args6.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args6.jsonnet b/testdata/optional_args6.jsonnet new file mode 100644 index 000000000..8ba581436 --- /dev/null +++ b/testdata/optional_args6.jsonnet @@ -0,0 +1,2 @@ +local foo(x, y=x) = y; +foo(x=42) diff --git a/testdata/optional_args7.golden b/testdata/optional_args7.golden new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/testdata/optional_args7.golden @@ -0,0 +1 @@ +42 diff --git a/testdata/optional_args7.jsonnet b/testdata/optional_args7.jsonnet new file mode 100644 index 000000000..18eeba6e4 --- /dev/null +++ b/testdata/optional_args7.jsonnet @@ -0,0 +1,2 @@ +local foo(x) = x; +foo(x=42) diff --git a/testdata/optional_args8.golden b/testdata/optional_args8.golden new file mode 100644 index 000000000..16d40b2f9 --- /dev/null +++ b/testdata/optional_args8.golden @@ -0,0 +1,10 @@ +RUNTIME ERROR: Function has no parameter y +------------------------------------------------- + testdata/optional_args8:2:1-10 $ + +foo(y=17) + +------------------------------------------------- + During evaluation + + diff --git a/testdata/optional_args8.jsonnet b/testdata/optional_args8.jsonnet new file mode 100644 index 000000000..5e0f2528e --- /dev/null +++ b/testdata/optional_args8.jsonnet @@ -0,0 +1,2 @@ +local foo(x=42) = x; +foo(y=17) diff --git a/testdata/optional_args9.golden b/testdata/optional_args9.golden new file mode 100644 index 000000000..17b4bbe7f --- /dev/null +++ b/testdata/optional_args9.golden @@ -0,0 +1,10 @@ +RUNTIME ERROR: Argument x already provided +------------------------------------------------- + testdata/optional_args9:1:1-26 $ + +(function(x) x)(42, x=42) + +------------------------------------------------- + During evaluation + + diff --git a/testdata/optional_args9.jsonnet b/testdata/optional_args9.jsonnet new file mode 100644 index 000000000..0db2a3601 --- /dev/null +++ b/testdata/optional_args9.jsonnet @@ -0,0 +1 @@ +(function(x) x)(42, x=42) diff --git a/testdata/std.makeArrayNamed.golden b/testdata/std.makeArrayNamed.golden new file mode 100644 index 000000000..1a9ceb7fa --- /dev/null +++ b/testdata/std.makeArrayNamed.golden @@ -0,0 +1,7 @@ +[ + 0, + 1, + 2, + 3, + 4 +] diff --git a/testdata/std.makeArrayNamed.jsonnet b/testdata/std.makeArrayNamed.jsonnet new file mode 100644 index 000000000..95e1f7a47 --- /dev/null +++ b/testdata/std.makeArrayNamed.jsonnet @@ -0,0 +1 @@ +std.makeArray(sz=5, func=function(i) i) diff --git a/testdata/std.makeArrayNamed2.golden b/testdata/std.makeArrayNamed2.golden new file mode 100644 index 000000000..1a9ceb7fa --- /dev/null +++ b/testdata/std.makeArrayNamed2.golden @@ -0,0 +1,7 @@ +[ + 0, + 1, + 2, + 3, + 4 +] diff --git a/testdata/std.makeArrayNamed2.jsonnet b/testdata/std.makeArrayNamed2.jsonnet new file mode 100644 index 000000000..bb5acb7f2 --- /dev/null +++ b/testdata/std.makeArrayNamed2.jsonnet @@ -0,0 +1 @@ +std.makeArray(func=function(i) i, sz=5) diff --git a/testdata/std.makeArrayNamed3.golden b/testdata/std.makeArrayNamed3.golden new file mode 100644 index 000000000..bcbfa36d6 --- /dev/null +++ b/testdata/std.makeArrayNamed3.golden @@ -0,0 +1,10 @@ +RUNTIME ERROR: Function has no parameter blahblah +------------------------------------------------- + testdata/std.makeArrayNamed3:1:1-54 $ + +std.makeArray(blahblah=5, blahblahblah=function(i) i) + +------------------------------------------------- + During evaluation + + diff --git a/testdata/std.makeArrayNamed3.jsonnet b/testdata/std.makeArrayNamed3.jsonnet new file mode 100644 index 000000000..e327cb964 --- /dev/null +++ b/testdata/std.makeArrayNamed3.jsonnet @@ -0,0 +1 @@ +std.makeArray(blahblah=5, blahblahblah=function(i) i) diff --git a/testdata/std.makeArrayNamed4.golden b/testdata/std.makeArrayNamed4.golden new file mode 100644 index 000000000..177436b81 --- /dev/null +++ b/testdata/std.makeArrayNamed4.golden @@ -0,0 +1,5 @@ +[ + 0, + 1, + 2 +] diff --git a/testdata/std.makeArrayNamed4.jsonnet b/testdata/std.makeArrayNamed4.jsonnet new file mode 100644 index 000000000..aabecf3d4 --- /dev/null +++ b/testdata/std.makeArrayNamed4.jsonnet @@ -0,0 +1 @@ +std.makeArray(3, func=function(i) i) diff --git a/testdata/std.toString8.golden b/testdata/std.toString8.golden new file mode 100644 index 000000000..192548e94 --- /dev/null +++ b/testdata/std.toString8.golden @@ -0,0 +1 @@ +"42" diff --git a/testdata/std.toString8.jsonnet b/testdata/std.toString8.jsonnet new file mode 100644 index 000000000..5dec3a16a --- /dev/null +++ b/testdata/std.toString8.jsonnet @@ -0,0 +1 @@ +std.toString(a=42) diff --git a/testdata/too_many_arguments.golden b/testdata/too_many_arguments.golden new file mode 100644 index 000000000..40661630d --- /dev/null +++ b/testdata/too_many_arguments.golden @@ -0,0 +1,10 @@ +RUNTIME ERROR: Function expected 3 positional argument(s), but got 4 +------------------------------------------------- + testdata/too_many_arguments:1:1-35 $ + +(function(x, y, z) 42)(1, 2, 3, 4) + +------------------------------------------------- + During evaluation + + diff --git a/testdata/too_many_arguments.jsonnet b/testdata/too_many_arguments.jsonnet new file mode 100644 index 000000000..52c30d7ce --- /dev/null +++ b/testdata/too_many_arguments.jsonnet @@ -0,0 +1 @@ +(function(x, y, z) 42)(1, 2, 3, 4) diff --git a/thunks.go b/thunks.go index 778eedd0d..f2b0a5672 100644 --- a/thunks.go +++ b/thunks.go @@ -96,6 +96,18 @@ func (th *callThunk) getValue(i *interpreter, trace *TraceElement) (value, error 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) +} + // 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, @@ -190,28 +202,81 @@ type closure struct { // arguments should be added to it, before executing it env environment function *ast.Function + params Parameters } func (closure *closure) EvalCall(arguments callArguments, e *evaluator) (value, error) { argThunks := make(bindingFrame) + parameters := closure.Parameters() for i, arg := range arguments.positional { - argThunks[closure.function.Parameters.Positional[i]] = arg + var name ast.Identifier + if i < len(parameters.required) { + name = parameters.required[i] + } else { + name = parameters.optional[i-len(parameters.required)].name + } + argThunks[name] = arg } - calledEnvironment := makeEnvironment( + for _, arg := range arguments.named { + argThunks[arg.name] = arg.pv + } + + var calledEnvironment environment + + for i := range parameters.optional { + param := ¶meters.optional[i] + if _, exists := argThunks[param.name]; !exists { + argThunks[param.name] = makeDeferredThunk(func() evaluable { + // Default arguments are evaluated in the same environment as function body + return param.defaultArg.inEnv(&calledEnvironment) + }) + } + } + + calledEnvironment = makeEnvironment( addBindings(closure.env.upValues, argThunks), closure.env.sb, ) return e.evalInCleanEnv(&calledEnvironment, closure.function.Body) } -func (closure *closure) Parameters() ast.Identifiers { - return closure.function.Parameters.Positional +func (closure *closure) Parameters() Parameters { + return closure.params + +} + +func prepareClosureParameters(parameters ast.Parameters, env environment) Parameters { + optionalParameters := make([]namedParameter, 0, len(parameters.Optional)) + for _, named := range parameters.Optional { + optionalParameters = append(optionalParameters, namedParameter{ + name: named.Name, + defaultArg: &defaultArgument{ + body: named.DefaultArg, + }, + }) + } + return Parameters{ + required: parameters.Required, + optional: optionalParameters, + } } func makeClosure(env environment, function *ast.Function) *closure { return &closure{ env: env, function: function, + params: prepareClosureParameters(function.Parameters, env), } } + +// partialPotentialValue +// ------------------------------------- + +type defaultArgument struct { + body ast.Node +} + +func (da *defaultArgument) inEnv(env *environment) potentialValue { + return makeThunk(*env, da.body) +} diff --git a/value.go b/value.go index 4b62e8acf..091a7eeea 100644 --- a/value.go +++ b/value.go @@ -235,24 +235,64 @@ type valueFunction struct { // TODO(sbarzowski) better name? type evalCallable interface { EvalCall(args callArguments, e *evaluator) (value, error) - Parameters() ast.Identifiers + Parameters() Parameters +} + +type partialPotentialValue interface { + inEnv(env *environment) potentialValue } func (f *valueFunction) call(args callArguments) potentialValue { return makeCallThunk(f.ec, args) } -func (f *valueFunction) parameters() ast.Identifiers { +func (f *valueFunction) parameters() Parameters { return f.ec.Parameters() } -func checkArguments(e *evaluator, args callArguments, params ast.Identifiers) error { - // TODO(sbarzowski) this will get much more complicated with named params +func checkArguments(e *evaluator, args callArguments, params Parameters) error { + received := make(map[ast.Identifier]bool) + accepted := make(map[ast.Identifier]bool) + numPassed := len(args.positional) - numExpected := len(params) - if numPassed != numExpected { - return e.Error(fmt.Sprintf("function expected %v argument(s), but got %v", numExpected, numPassed)) + 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)) + } + + for _, param := range params.required { + accepted[param] = true + } + + for _, param := range params.optional { + accepted[param.name] = true + } + + for i := range args.positional { + if i < len(params.required) { + received[params.required[i]] = true + } else { + received[params.optional[i-len(params.required)].name] = true + } } + + for _, arg := range args.named { + if _, present := received[arg.name]; present { + return e.Error(fmt.Sprintf("Argument %v already provided", arg.name)) + } + if _, present := accepted[arg.name]; !present { + return e.Error(fmt.Sprintf("Function has no parameter %v", arg.name)) + } + received[arg.name] = true + } + + for _, param := range params.required { + if _, present := received[param]; !present { + return e.Error(fmt.Sprintf("Missing argument: %v", param)) + } + } + return nil } @@ -260,9 +300,28 @@ func (f *valueFunction) typename() string { return "function" } +type Parameters struct { + required ast.Identifiers + optional []namedParameter +} + +type namedParameter struct { + name ast.Identifier + defaultArg potentialValueInEnv +} + +type potentialValueInEnv interface { + inEnv(env *environment) potentialValue +} + type callArguments struct { positional []potentialValue - // TODO named arguments + named []namedCallArgument +} + +type namedCallArgument struct { + name ast.Identifier + pv potentialValue } func args(xs ...potentialValue) callArguments {