From 78b479452361d56b5993005ab8596ed471cafe27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Barzowski?= Date: Wed, 20 Sep 2017 14:58:38 -0400 Subject: [PATCH] Object comprehensions --- ast/ast.go | 12 ----- builtins.go | 39 ++++++++++++++ desugarer.go | 69 +++++++++++++++++-------- interpreter.go | 4 +- static_analyzer.go | 3 -- testdata/fieldname_not_string.golden | 2 +- testdata/object_comp.golden | 5 ++ testdata/object_comp.jsonnet | 1 + testdata/object_comp2.golden | 1 + testdata/object_comp2.jsonnet | 1 + testdata/object_comp3.golden | 5 ++ testdata/object_comp3.jsonnet | 1 + testdata/object_comp_assert.golden | 1 + testdata/object_comp_assert.jsonnet | 1 + testdata/object_comp_bad_field.golden | 1 + testdata/object_comp_bad_field.jsonnet | 1 + testdata/object_comp_bad_field2.golden | 1 + testdata/object_comp_bad_field2.jsonnet | 1 + testdata/object_comp_duplicate.golden | 1 + testdata/object_comp_duplicate.jsonnet | 1 + testdata/object_comp_err_elem.golden | 1 + testdata/object_comp_err_elem.jsonnet | 1 + testdata/object_comp_err_index.golden | 1 + testdata/object_comp_err_index.jsonnet | 1 + testdata/object_comp_if.golden | 4 ++ testdata/object_comp_if.jsonnet | 1 + testdata/object_comp_illegal.golden | 1 + testdata/object_comp_illegal.jsonnet | 1 + testdata/object_comp_int_index.golden | 1 + testdata/object_comp_int_index.jsonnet | 1 + testdata/object_comp_super.golden | 18 +++++++ testdata/object_comp_super.jsonnet | 4 ++ testdata/proto_object_comp.golden | 11 ++++ testdata/proto_object_comp.jsonnet | 1 + testdata/string_to_bool.golden | 4 ++ testdata/string_to_bool.jsonnet | 5 ++ thunks.go | 23 ++++++++- value.go | 4 ++ vm.go | 15 +++--- 39 files changed, 200 insertions(+), 49 deletions(-) create mode 100644 testdata/object_comp.golden create mode 100644 testdata/object_comp.jsonnet create mode 100644 testdata/object_comp2.golden create mode 100644 testdata/object_comp2.jsonnet create mode 100644 testdata/object_comp3.golden create mode 100644 testdata/object_comp3.jsonnet create mode 100644 testdata/object_comp_assert.golden create mode 100644 testdata/object_comp_assert.jsonnet create mode 100644 testdata/object_comp_bad_field.golden create mode 100644 testdata/object_comp_bad_field.jsonnet create mode 100644 testdata/object_comp_bad_field2.golden create mode 100644 testdata/object_comp_bad_field2.jsonnet create mode 100644 testdata/object_comp_duplicate.golden create mode 100644 testdata/object_comp_duplicate.jsonnet create mode 100644 testdata/object_comp_err_elem.golden create mode 100644 testdata/object_comp_err_elem.jsonnet create mode 100644 testdata/object_comp_err_index.golden create mode 100644 testdata/object_comp_err_index.jsonnet create mode 100644 testdata/object_comp_if.golden create mode 100644 testdata/object_comp_if.jsonnet create mode 100644 testdata/object_comp_illegal.golden create mode 100644 testdata/object_comp_illegal.jsonnet create mode 100644 testdata/object_comp_int_index.golden create mode 100644 testdata/object_comp_int_index.jsonnet create mode 100644 testdata/object_comp_super.golden create mode 100644 testdata/object_comp_super.jsonnet create mode 100644 testdata/proto_object_comp.golden create mode 100644 testdata/proto_object_comp.jsonnet create mode 100644 testdata/string_to_bool.golden create mode 100644 testdata/string_to_bool.jsonnet diff --git a/ast/ast.go b/ast/ast.go index 546aa2d..862bdeb 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -482,18 +482,6 @@ type ObjectComp struct { // --------------------------------------------------------------------------- -// ObjectComprehensionSimple represents post-desugaring object -// comprehension { [e]: e for x in e }. -type ObjectComprehensionSimple struct { - NodeBase - Field Node - Value Node - Id Identifier - Array Node -} - -// --------------------------------------------------------------------------- - // Self represents the self keyword. type Self struct{ NodeBase } diff --git a/builtins.go b/builtins.go index 8480e6c..eed46fa 100644 --- a/builtins.go +++ b/builtins.go @@ -553,6 +553,42 @@ func builtinPow(e *evaluator, basep potentialValue, expp potentialValue) (value, return makeDoubleCheck(e, math.Pow(base.value, exp.value)) } +func builtinUglyObjectFlatMerge(e *evaluator, objarrp potentialValue) (value, error) { + objarr, err := e.evaluateArray(objarrp) + if err != nil { + return nil, err + } + if len(objarr.elements) == 0 { + return &valueSimpleObject{}, nil + } + newFields := make(valueSimpleObjectFieldMap) + for _, elem := range objarr.elements { + obj, err := e.evaluateObject(elem) + if err != nil { + return nil, err + } + // starts getting ugly - we mess with object internals + simpleObj := obj.(*valueSimpleObject) + for fieldName, fieldVal := range simpleObj.fields { + if _, alreadyExists := newFields[fieldName]; alreadyExists { + return nil, e.Error(duplicateFieldNameErrMsg(fieldName)) + } + newFields[fieldName] = valueSimpleObjectField{ + hide: fieldVal.hide, + field: &bindingsUnboundField{ + inner: fieldVal.field, + bindings: simpleObj.upValues, + }, + } + } + } + return makeValueSimpleObject( + nil, // no binding frame + newFields, + []unboundField{}, // No asserts allowed + ), nil +} + type unaryBuiltin func(*evaluator, potentialValue) (value, error) type binaryBuiltin func(*evaluator, potentialValue, potentialValue) (value, error) type ternaryBuiltin func(*evaluator, potentialValue, potentialValue, potentialValue) (value, error) @@ -686,4 +722,7 @@ var funcBuiltins = map[string]evalCallable{ "pow": &BinaryBuiltin{name: "pow", function: builtinPow, parameters: ast.Identifiers{"base", "exp"}}, "modulo": &BinaryBuiltin{name: "modulo", function: builtinModulo, parameters: ast.Identifiers{"x", "y"}}, "md5": &UnaryBuiltin{name: "md5", function: builtinMd5, parameters: ast.Identifiers{"x"}}, + + // internal + "$objectFlatMerge": &UnaryBuiltin{name: "$objectFlatMerge", function: builtinUglyObjectFlatMerge, parameters: ast.Identifiers{"x"}}, } diff --git a/desugarer.go b/desugarer.go index b14e073..fb7fecd 100644 --- a/desugarer.go +++ b/desugarer.go @@ -249,9 +249,31 @@ func desugarArrayComp(comp *ast.ArrayComp, objLevel int) (ast.Node, error) { return desugarForSpec(wrapInArray(comp.Body), &comp.Spec) } -func desugarObjectComp(astComp *ast.ObjectComp, objLevel int) (ast.Node, error) { - return &ast.LiteralNull{}, nil - // TODO(sbarzowski) this +func desugarObjectComp(comp *ast.ObjectComp, objLevel int) (ast.Node, error) { + + // 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 + } + + if len(comp.Fields) != 1 { + panic("Too many fields in object comprehension, it should have been caught during parsing") + } + + arrComp := ast.ArrayComp{ + Body: buildDesugaredObject(comp.NodeBase, comp.Fields), + Spec: comp.Spec, + } + + desugaredArrayComp, err := desugarArrayComp(&arrComp, objLevel) + if err != nil { + return nil, err + } + + desugaredComp := buildStdCall("$objectFlatMerge", desugaredArrayComp) + return desugaredComp, nil } func buildLiteralString(value string) ast.Node { @@ -277,6 +299,23 @@ func buildStdCall(builtinName ast.Identifier, args ...ast.Node) ast.Node { } } +func buildDesugaredObject(nodeBase ast.NodeBase, fields ast.ObjectFields) *ast.DesugaredObject { + var newFields ast.DesugaredObjectFields + var newAsserts ast.Nodes + + for _, field := range fields { + if field.Kind == ast.ObjectAssert { + newAsserts = append(newAsserts, field.Expr2) + } else if field.Kind == ast.ObjectFieldExpr { + newFields = append(newFields, ast.DesugaredObjectField{field.Hide, field.Expr1, field.Expr2}) + } else { + panic(fmt.Sprintf("INTERNAL ERROR: field should have been desugared: %s", field.Kind)) + } + } + + return &ast.DesugaredObject{nodeBase, newAsserts, newFields} +} + // Desugar Jsonnet expressions to reduce the number of constructs the rest of the implementation // needs to understand. @@ -506,34 +545,22 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) { return } - var newFields ast.DesugaredObjectFields - var newAsserts ast.Nodes - - for _, field := range node.Fields { - if field.Kind == ast.ObjectAssert { - newAsserts = append(newAsserts, field.Expr2) - } else if field.Kind == ast.ObjectFieldExpr { - newFields = append(newFields, ast.DesugaredObjectField{field.Hide, field.Expr1, field.Expr2}) - } else { - panic(fmt.Sprintf("INTERNAL ERROR: field should have been desugared: %s", field.Kind)) - } - } - - *astPtr = &ast.DesugaredObject{node.NodeBase, newAsserts, newFields} + *astPtr = buildDesugaredObject(node.NodeBase, node.Fields) case *ast.DesugaredObject: - panic("Desugaring desugared object") + return nil case *ast.ObjectComp: comp, err := desugarObjectComp(node, objLevel) if err != nil { return err } + err = desugar(&comp, objLevel) + if err != nil { + return err + } *astPtr = comp - case *ast.ObjectComprehensionSimple: - panic("Desugaring desugared object comprehension") - case *ast.Self: // Nothing to do. diff --git a/interpreter.go b/interpreter.go index 94f5d1a..5b53d6a 100644 --- a/interpreter.go +++ b/interpreter.go @@ -318,11 +318,11 @@ func (i *interpreter) evaluate(a ast.Node, context *TraceContext) (value, error) // Omitted field. continue default: - return nil, e.Error("Field name was not a string.") + return nil, e.Error(fmt.Sprintf("Field name must be string, got %v", fieldNameValue.typename())) } if _, ok := fields[fieldName]; ok { - return nil, e.Error(fmt.Sprintf("Duplicate field name: \"%s\"", fieldName)) + return nil, e.Error(duplicateFieldNameErrMsg(fieldName)) } fields[fieldName] = valueSimpleObjectField{field.Hide, &codeUnboundField{field.Body}} } diff --git a/static_analyzer.go b/static_analyzer.go index 74cc0a3..bc295f3 100644 --- a/static_analyzer.go +++ b/static_analyzer.go @@ -117,9 +117,6 @@ func analyzeVisit(a ast.Node, inObject bool, vars ast.IdentifierSet) error { for _, assert := range a.Asserts { visitNext(assert, true, vars, s) } - case *ast.ObjectComprehensionSimple: - // TODO (sbarzowski) this - panic("Comprehensions not supported yet") case *ast.Self: if !inObject { return parser.MakeStaticError("Can't use self outside of an object.", *a.Loc()) diff --git a/testdata/fieldname_not_string.golden b/testdata/fieldname_not_string.golden index 7752dce..eba5cc7 100644 --- a/testdata/fieldname_not_string.golden +++ b/testdata/fieldname_not_string.golden @@ -1 +1 @@ -RUNTIME ERROR: Field name was not a string. +RUNTIME ERROR: Field name must be string, got number diff --git a/testdata/object_comp.golden b/testdata/object_comp.golden new file mode 100644 index 0000000..3c420a1 --- /dev/null +++ b/testdata/object_comp.golden @@ -0,0 +1,5 @@ +{ + "a": 42, + "b": 42, + "c": 42 +} diff --git a/testdata/object_comp.jsonnet b/testdata/object_comp.jsonnet new file mode 100644 index 0000000..87814a0 --- /dev/null +++ b/testdata/object_comp.jsonnet @@ -0,0 +1 @@ +{ [x]: 42 for x in ["a", "b", "c"] } diff --git a/testdata/object_comp2.golden b/testdata/object_comp2.golden new file mode 100644 index 0000000..ffcd441 --- /dev/null +++ b/testdata/object_comp2.golden @@ -0,0 +1 @@ +{ } diff --git a/testdata/object_comp2.jsonnet b/testdata/object_comp2.jsonnet new file mode 100644 index 0000000..604c886 --- /dev/null +++ b/testdata/object_comp2.jsonnet @@ -0,0 +1 @@ +{ [x]: error "xxx" for x in [] } diff --git a/testdata/object_comp3.golden b/testdata/object_comp3.golden new file mode 100644 index 0000000..c68fdd0 --- /dev/null +++ b/testdata/object_comp3.golden @@ -0,0 +1,5 @@ +{ + "x": "x", + "y": "y", + "z": "z" +} diff --git a/testdata/object_comp3.jsonnet b/testdata/object_comp3.jsonnet new file mode 100644 index 0000000..4ce598c --- /dev/null +++ b/testdata/object_comp3.jsonnet @@ -0,0 +1 @@ +{[v]: v for v in ["x", "y", "z"] } diff --git a/testdata/object_comp_assert.golden b/testdata/object_comp_assert.golden new file mode 100644 index 0000000..7d94a53 --- /dev/null +++ b/testdata/object_comp_assert.golden @@ -0,0 +1 @@ +testdata/object_comp_assert:1:32-35 Object comprehension cannot have asserts. diff --git a/testdata/object_comp_assert.jsonnet b/testdata/object_comp_assert.jsonnet new file mode 100644 index 0000000..9a8a2a3 --- /dev/null +++ b/testdata/object_comp_assert.jsonnet @@ -0,0 +1 @@ +{ assert self.x == 5, ["x"]: 5 for i in [42] } diff --git a/testdata/object_comp_bad_field.golden b/testdata/object_comp_bad_field.golden new file mode 100644 index 0000000..355c625 --- /dev/null +++ b/testdata/object_comp_bad_field.golden @@ -0,0 +1 @@ +testdata/object_comp_bad_field:1:9-12 Object comprehensions can only have [e] fields. diff --git a/testdata/object_comp_bad_field.jsonnet b/testdata/object_comp_bad_field.jsonnet new file mode 100644 index 0000000..71bc4d9 --- /dev/null +++ b/testdata/object_comp_bad_field.jsonnet @@ -0,0 +1 @@ +{ x: 42 for _ in [1] } diff --git a/testdata/object_comp_bad_field2.golden b/testdata/object_comp_bad_field2.golden new file mode 100644 index 0000000..0119446 --- /dev/null +++ b/testdata/object_comp_bad_field2.golden @@ -0,0 +1 @@ +testdata/object_comp_bad_field2:1:11-14 Object comprehensions can only have [e] fields. diff --git a/testdata/object_comp_bad_field2.jsonnet b/testdata/object_comp_bad_field2.jsonnet new file mode 100644 index 0000000..bce97b9 --- /dev/null +++ b/testdata/object_comp_bad_field2.jsonnet @@ -0,0 +1 @@ +{ "x": 42 for _ in [1] } diff --git a/testdata/object_comp_duplicate.golden b/testdata/object_comp_duplicate.golden new file mode 100644 index 0000000..6b0fab0 --- /dev/null +++ b/testdata/object_comp_duplicate.golden @@ -0,0 +1 @@ +RUNTIME ERROR: Duplicate field name: "x" diff --git a/testdata/object_comp_duplicate.jsonnet b/testdata/object_comp_duplicate.jsonnet new file mode 100644 index 0000000..2e06667 --- /dev/null +++ b/testdata/object_comp_duplicate.jsonnet @@ -0,0 +1 @@ +{ [x]: x for x in ["x", "x"] } diff --git a/testdata/object_comp_err_elem.golden b/testdata/object_comp_err_elem.golden new file mode 100644 index 0000000..7bc34d8 --- /dev/null +++ b/testdata/object_comp_err_elem.golden @@ -0,0 +1 @@ +RUNTIME ERROR: xxx diff --git a/testdata/object_comp_err_elem.jsonnet b/testdata/object_comp_err_elem.jsonnet new file mode 100644 index 0000000..78af03a --- /dev/null +++ b/testdata/object_comp_err_elem.jsonnet @@ -0,0 +1 @@ +{ ["x"]: error "xxx" for x in [1] } diff --git a/testdata/object_comp_err_index.golden b/testdata/object_comp_err_index.golden new file mode 100644 index 0000000..7bc34d8 --- /dev/null +++ b/testdata/object_comp_err_index.golden @@ -0,0 +1 @@ +RUNTIME ERROR: xxx diff --git a/testdata/object_comp_err_index.jsonnet b/testdata/object_comp_err_index.jsonnet new file mode 100644 index 0000000..0231687 --- /dev/null +++ b/testdata/object_comp_err_index.jsonnet @@ -0,0 +1 @@ +{ [error "xxx"]: 42 for x in [1] } diff --git a/testdata/object_comp_if.golden b/testdata/object_comp_if.golden new file mode 100644 index 0000000..ba5f08c --- /dev/null +++ b/testdata/object_comp_if.golden @@ -0,0 +1,4 @@ +{ + "b": 42, + "bb": 42 +} diff --git a/testdata/object_comp_if.jsonnet b/testdata/object_comp_if.jsonnet new file mode 100644 index 0000000..b8ef328 --- /dev/null +++ b/testdata/object_comp_if.jsonnet @@ -0,0 +1 @@ +{ [x]: 42 for x in ["a", "b", "bb", "c"] if x[0] == "b" } diff --git a/testdata/object_comp_illegal.golden b/testdata/object_comp_illegal.golden new file mode 100644 index 0000000..34d61d3 --- /dev/null +++ b/testdata/object_comp_illegal.golden @@ -0,0 +1 @@ +testdata/object_comp_illegal:1:15-18 Object comprehension can only have one field. diff --git a/testdata/object_comp_illegal.jsonnet b/testdata/object_comp_illegal.jsonnet new file mode 100644 index 0000000..3906d7c --- /dev/null +++ b/testdata/object_comp_illegal.jsonnet @@ -0,0 +1 @@ +{ local x = 5 for y in [1, 2, 3] } diff --git a/testdata/object_comp_int_index.golden b/testdata/object_comp_int_index.golden new file mode 100644 index 0000000..eba5cc7 --- /dev/null +++ b/testdata/object_comp_int_index.golden @@ -0,0 +1 @@ +RUNTIME ERROR: Field name must be string, got number diff --git a/testdata/object_comp_int_index.jsonnet b/testdata/object_comp_int_index.jsonnet new file mode 100644 index 0000000..155a6da --- /dev/null +++ b/testdata/object_comp_int_index.jsonnet @@ -0,0 +1 @@ +{ [x]: x for x in [1, 2, 3] } diff --git a/testdata/object_comp_super.golden b/testdata/object_comp_super.golden new file mode 100644 index 0000000..82b3604 --- /dev/null +++ b/testdata/object_comp_super.golden @@ -0,0 +1,18 @@ +{ + "a": "42a", + "b": "42b", + "c": "42c", + "q": 42, + "x": [ + 1, + "x" + ], + "y": [ + 1, + "y" + ], + "z": [ + 1, + "z" + ] +} diff --git a/testdata/object_comp_super.jsonnet b/testdata/object_comp_super.jsonnet new file mode 100644 index 0000000..73830db --- /dev/null +++ b/testdata/object_comp_super.jsonnet @@ -0,0 +1,4 @@ +{x: 1, y: 2, z: 3} + +{[v]: [super.x, v] for v in ["x", "y", "z"] } + +{[v]: self.q + v for v in ["a", "b", "c"]} + +{q: 42} diff --git a/testdata/proto_object_comp.golden b/testdata/proto_object_comp.golden new file mode 100644 index 0000000..a505401 --- /dev/null +++ b/testdata/proto_object_comp.golden @@ -0,0 +1,11 @@ +[ + { + "x": "x" + }, + { + "y": "y" + }, + { + "z": "z" + } +] diff --git a/testdata/proto_object_comp.jsonnet b/testdata/proto_object_comp.jsonnet new file mode 100644 index 0000000..dcbab62 --- /dev/null +++ b/testdata/proto_object_comp.jsonnet @@ -0,0 +1 @@ +[{[v]: v} for v in ["x", "y", "z"]] diff --git a/testdata/string_to_bool.golden b/testdata/string_to_bool.golden new file mode 100644 index 0000000..1da2ae5 --- /dev/null +++ b/testdata/string_to_bool.golden @@ -0,0 +1,4 @@ +[ + false, + true +] diff --git a/testdata/string_to_bool.jsonnet b/testdata/string_to_bool.jsonnet new file mode 100644 index 0000000..0d0ad65 --- /dev/null +++ b/testdata/string_to_bool.jsonnet @@ -0,0 +1,5 @@ +local stringToBool(s) = + if s == "true" then true + else if s == "false" then false + else error "invalid boolean: " + std.manifestJson(s); +[stringToBool("false"), stringToBool("true")] diff --git a/thunks.go b/thunks.go index b26fb21..759f4e6 100644 --- a/thunks.go +++ b/thunks.go @@ -131,9 +131,28 @@ type codeUnboundField struct { body ast.Node } -func (f *codeUnboundField) bindToObject(sb selfBinding, origBinding bindingFrame) potentialValue { +func (f *codeUnboundField) bindToObject(sb selfBinding, origBindings bindingFrame) potentialValue { // TODO(sbarzowski) better object names (perhaps include a field name too?) - return makeThunk("object_field", makeEnvironment(origBinding, sb), f.body) + return makeThunk("object_field", makeEnvironment(origBindings, sb), f.body) +} + +// Provide additional bindings for a field. It shadows bindings from the object. +type bindingsUnboundField struct { + inner unboundField + // in addition to "generic" binding frame from the object + bindings bindingFrame +} + +func (f *bindingsUnboundField) bindToObject(sb selfBinding, origBindings bindingFrame) potentialValue { + var upValues bindingFrame + upValues = make(bindingFrame) + for variable, pvalue := range origBindings { + upValues[variable] = pvalue + } + for variable, pvalue := range f.bindings { + upValues[variable] = pvalue + } + return f.inner.bindToObject(sb, upValues) } // evalCallables diff --git a/value.go b/value.go index f7ff4d0..f514f49 100644 --- a/value.go +++ b/value.go @@ -543,3 +543,7 @@ func objectFields(obj valueObject, manifesting bool) []string { } return r } + +func duplicateFieldNameErrMsg(fieldName string) string { + return fmt.Sprintf("Duplicate field name: %s", unparseString(fieldName)) +} diff --git a/vm.go b/vm.go index 3e52a0d..34c5f37 100644 --- a/vm.go +++ b/vm.go @@ -69,12 +69,17 @@ func (vm *VM) ExtCode(key string, val string) { vm.ext[key] = vmExt{value: val, isCode: true} } -func (vm *VM) evaluateSnippet(filename string, snippet string) (string, error) { +func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) + } + }() node, err := snippetToAST(filename, snippet) if err != nil { return "", err } - output, err := evaluate(node, vm.ext, vm.MaxStack, &FileImporter{}) + output, err = evaluate(node, vm.ext, vm.MaxStack, &FileImporter{}) if err != nil { return "", err } @@ -86,11 +91,6 @@ func (vm *VM) evaluateSnippet(filename string, snippet string) (string, error) { // // The filename parameter is only used for error messages. func (vm *VM) EvaluateSnippet(filename string, snippet string) (json string, formattedErr error) { - defer func() { - if r := recover(); r != nil { - formattedErr = errors.New(vm.ef.format(fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()))) - } - }() json, err := vm.evaluateSnippet(filename, snippet) if err != nil { return "", errors.New(vm.ef.format(err)) @@ -107,7 +107,6 @@ func snippetToAST(filename string, snippet string) (ast.Node, error) { if err != nil { return nil, err } - // fmt.Println(ast.(dumpable).dump()) err = desugarFile(&node) if err != nil { return nil, err