Object comprehensions

This commit is contained in:
Stanisław Barzowski 2017-09-20 14:58:38 -04:00 committed by Dave Cunningham
parent 850575cf34
commit 78b4794523
39 changed files with 200 additions and 49 deletions

View File

@ -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 }

View File

@ -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"}},
}

View File

@ -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.

View File

@ -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}}
}

View File

@ -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())

View File

@ -1 +1 @@
RUNTIME ERROR: Field name was not a string.
RUNTIME ERROR: Field name must be string, got number

5
testdata/object_comp.golden vendored Normal file
View File

@ -0,0 +1,5 @@
{
"a": 42,
"b": 42,
"c": 42
}

1
testdata/object_comp.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ [x]: 42 for x in ["a", "b", "c"] }

1
testdata/object_comp2.golden vendored Normal file
View File

@ -0,0 +1 @@
{ }

1
testdata/object_comp2.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ [x]: error "xxx" for x in [] }

5
testdata/object_comp3.golden vendored Normal file
View File

@ -0,0 +1,5 @@
{
"x": "x",
"y": "y",
"z": "z"
}

1
testdata/object_comp3.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{[v]: v for v in ["x", "y", "z"] }

1
testdata/object_comp_assert.golden vendored Normal file
View File

@ -0,0 +1 @@
testdata/object_comp_assert:1:32-35 Object comprehension cannot have asserts.

1
testdata/object_comp_assert.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ assert self.x == 5, ["x"]: 5 for i in [42] }

1
testdata/object_comp_bad_field.golden vendored Normal file
View File

@ -0,0 +1 @@
testdata/object_comp_bad_field:1:9-12 Object comprehensions can only have [e] fields.

View File

@ -0,0 +1 @@
{ x: 42 for _ in [1] }

View File

@ -0,0 +1 @@
testdata/object_comp_bad_field2:1:11-14 Object comprehensions can only have [e] fields.

View File

@ -0,0 +1 @@
{ "x": 42 for _ in [1] }

1
testdata/object_comp_duplicate.golden vendored Normal file
View File

@ -0,0 +1 @@
RUNTIME ERROR: Duplicate field name: "x"

View File

@ -0,0 +1 @@
{ [x]: x for x in ["x", "x"] }

1
testdata/object_comp_err_elem.golden vendored Normal file
View File

@ -0,0 +1 @@
RUNTIME ERROR: xxx

1
testdata/object_comp_err_elem.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ ["x"]: error "xxx" for x in [1] }

1
testdata/object_comp_err_index.golden vendored Normal file
View File

@ -0,0 +1 @@
RUNTIME ERROR: xxx

View File

@ -0,0 +1 @@
{ [error "xxx"]: 42 for x in [1] }

4
testdata/object_comp_if.golden vendored Normal file
View File

@ -0,0 +1,4 @@
{
"b": 42,
"bb": 42
}

1
testdata/object_comp_if.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ [x]: 42 for x in ["a", "b", "bb", "c"] if x[0] == "b" }

1
testdata/object_comp_illegal.golden vendored Normal file
View File

@ -0,0 +1 @@
testdata/object_comp_illegal:1:15-18 Object comprehension can only have one field.

1
testdata/object_comp_illegal.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ local x = 5 for y in [1, 2, 3] }

1
testdata/object_comp_int_index.golden vendored Normal file
View File

@ -0,0 +1 @@
RUNTIME ERROR: Field name must be string, got number

View File

@ -0,0 +1 @@
{ [x]: x for x in [1, 2, 3] }

18
testdata/object_comp_super.golden vendored Normal file
View File

@ -0,0 +1,18 @@
{
"a": "42a",
"b": "42b",
"c": "42c",
"q": 42,
"x": [
1,
"x"
],
"y": [
1,
"y"
],
"z": [
1,
"z"
]
}

4
testdata/object_comp_super.jsonnet vendored Normal file
View File

@ -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}

11
testdata/proto_object_comp.golden vendored Normal file
View File

@ -0,0 +1,11 @@
[
{
"x": "x"
},
{
"y": "y"
},
{
"z": "z"
}
]

1
testdata/proto_object_comp.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
[{[v]: v} for v in ["x", "y", "z"]]

4
testdata/string_to_bool.golden vendored Normal file
View File

@ -0,0 +1,4 @@
[
false,
true
]

5
testdata/string_to_bool.jsonnet vendored Normal file
View File

@ -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")]

View File

@ -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

View File

@ -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))
}

15
vm.go
View File

@ -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