mirror of
https://github.com/google/go-jsonnet.git
synced 2025-09-28 17:01:02 +02:00
Object comprehensions
This commit is contained in:
parent
850575cf34
commit
78b4794523
12
ast/ast.go
12
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 }
|
||||
|
||||
|
39
builtins.go
39
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"}},
|
||||
}
|
||||
|
69
desugarer.go
69
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.
|
||||
|
||||
|
@ -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}}
|
||||
}
|
||||
|
@ -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())
|
||||
|
2
testdata/fieldname_not_string.golden
vendored
2
testdata/fieldname_not_string.golden
vendored
@ -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
5
testdata/object_comp.golden
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"a": 42,
|
||||
"b": 42,
|
||||
"c": 42
|
||||
}
|
1
testdata/object_comp.jsonnet
vendored
Normal file
1
testdata/object_comp.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ [x]: 42 for x in ["a", "b", "c"] }
|
1
testdata/object_comp2.golden
vendored
Normal file
1
testdata/object_comp2.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ }
|
1
testdata/object_comp2.jsonnet
vendored
Normal file
1
testdata/object_comp2.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ [x]: error "xxx" for x in [] }
|
5
testdata/object_comp3.golden
vendored
Normal file
5
testdata/object_comp3.golden
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"x": "x",
|
||||
"y": "y",
|
||||
"z": "z"
|
||||
}
|
1
testdata/object_comp3.jsonnet
vendored
Normal file
1
testdata/object_comp3.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{[v]: v for v in ["x", "y", "z"] }
|
1
testdata/object_comp_assert.golden
vendored
Normal file
1
testdata/object_comp_assert.golden
vendored
Normal 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
1
testdata/object_comp_assert.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ assert self.x == 5, ["x"]: 5 for i in [42] }
|
1
testdata/object_comp_bad_field.golden
vendored
Normal file
1
testdata/object_comp_bad_field.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
testdata/object_comp_bad_field:1:9-12 Object comprehensions can only have [e] fields.
|
1
testdata/object_comp_bad_field.jsonnet
vendored
Normal file
1
testdata/object_comp_bad_field.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ x: 42 for _ in [1] }
|
1
testdata/object_comp_bad_field2.golden
vendored
Normal file
1
testdata/object_comp_bad_field2.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
testdata/object_comp_bad_field2:1:11-14 Object comprehensions can only have [e] fields.
|
1
testdata/object_comp_bad_field2.jsonnet
vendored
Normal file
1
testdata/object_comp_bad_field2.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ "x": 42 for _ in [1] }
|
1
testdata/object_comp_duplicate.golden
vendored
Normal file
1
testdata/object_comp_duplicate.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
RUNTIME ERROR: Duplicate field name: "x"
|
1
testdata/object_comp_duplicate.jsonnet
vendored
Normal file
1
testdata/object_comp_duplicate.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ [x]: x for x in ["x", "x"] }
|
1
testdata/object_comp_err_elem.golden
vendored
Normal file
1
testdata/object_comp_err_elem.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
RUNTIME ERROR: xxx
|
1
testdata/object_comp_err_elem.jsonnet
vendored
Normal file
1
testdata/object_comp_err_elem.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ ["x"]: error "xxx" for x in [1] }
|
1
testdata/object_comp_err_index.golden
vendored
Normal file
1
testdata/object_comp_err_index.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
RUNTIME ERROR: xxx
|
1
testdata/object_comp_err_index.jsonnet
vendored
Normal file
1
testdata/object_comp_err_index.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ [error "xxx"]: 42 for x in [1] }
|
4
testdata/object_comp_if.golden
vendored
Normal file
4
testdata/object_comp_if.golden
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"b": 42,
|
||||
"bb": 42
|
||||
}
|
1
testdata/object_comp_if.jsonnet
vendored
Normal file
1
testdata/object_comp_if.jsonnet
vendored
Normal 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
1
testdata/object_comp_illegal.golden
vendored
Normal 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
1
testdata/object_comp_illegal.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ local x = 5 for y in [1, 2, 3] }
|
1
testdata/object_comp_int_index.golden
vendored
Normal file
1
testdata/object_comp_int_index.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
RUNTIME ERROR: Field name must be string, got number
|
1
testdata/object_comp_int_index.jsonnet
vendored
Normal file
1
testdata/object_comp_int_index.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ [x]: x for x in [1, 2, 3] }
|
18
testdata/object_comp_super.golden
vendored
Normal file
18
testdata/object_comp_super.golden
vendored
Normal 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
4
testdata/object_comp_super.jsonnet
vendored
Normal 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
11
testdata/proto_object_comp.golden
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"x": "x"
|
||||
},
|
||||
{
|
||||
"y": "y"
|
||||
},
|
||||
{
|
||||
"z": "z"
|
||||
}
|
||||
]
|
1
testdata/proto_object_comp.jsonnet
vendored
Normal file
1
testdata/proto_object_comp.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
[{[v]: v} for v in ["x", "y", "z"]]
|
4
testdata/string_to_bool.golden
vendored
Normal file
4
testdata/string_to_bool.golden
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[
|
||||
false,
|
||||
true
|
||||
]
|
5
testdata/string_to_bool.jsonnet
vendored
Normal file
5
testdata/string_to_bool.jsonnet
vendored
Normal 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")]
|
23
thunks.go
23
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
|
||||
|
4
value.go
4
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))
|
||||
}
|
||||
|
15
vm.go
15
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user