From 6f2bd0009c404feda9a787e7c9c31bde37b17318 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 4 Jun 2017 16:14:02 -0700 Subject: [PATCH] zclsyntax: parsing and evaluation for object constructors --- zcl/zclsyntax/expression.go | 95 ++++++++++++++++++++++ zcl/zclsyntax/expression_test.go | 93 ++++++++++++++++++++++ zcl/zclsyntax/expression_vars.go | 4 + zcl/zclsyntax/parser.go | 132 +++++++++++++++++++++++++++++++ 4 files changed, 324 insertions(+) diff --git a/zcl/zclsyntax/expression.go b/zcl/zclsyntax/expression.go index 7cc7b45..52e0f76 100644 --- a/zcl/zclsyntax/expression.go +++ b/zcl/zclsyntax/expression.go @@ -397,3 +397,98 @@ func (e *TupleConsExpr) Range() zcl.Range { func (e *TupleConsExpr) StartRange() zcl.Range { return e.OpenRange } + +type ObjectConsExpr struct { + Items []ObjectConsItem + + SrcRange zcl.Range + OpenRange zcl.Range +} + +type ObjectConsItem struct { + KeyExpr Expression + ValueExpr Expression +} + +func (e *ObjectConsExpr) walkChildNodes(w internalWalkFunc) { + for i, item := range e.Items { + e.Items[i].KeyExpr = w(item.KeyExpr).(Expression) + e.Items[i].ValueExpr = w(item.ValueExpr).(Expression) + } +} + +func (e *ObjectConsExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) { + var vals map[string]cty.Value + var diags zcl.Diagnostics + + // This will get set to true if we fail to produce any of our keys, + // either because they are actually unknown or if the evaluation produces + // errors. In all of these case we must return DynamicPseudoType because + // we're unable to know the full set of keys our object has, and thus + // we can't produce a complete value of the intended type. + // + // We still evaluate all of the item keys and values to make sure that we + // get as complete as possible a set of diagnostics. + known := true + + vals = make(map[string]cty.Value, len(e.Items)) + for _, item := range e.Items { + key, keyDiags := item.KeyExpr.Value(ctx) + diags = append(diags, keyDiags...) + + val, valDiags := item.ValueExpr.Value(ctx) + diags = append(diags, valDiags...) + + if keyDiags.HasErrors() { + known = false + continue + } + + if key.IsNull() { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Null value as key", + Detail: "Can't use a null value as a key.", + Subject: item.ValueExpr.Range().Ptr(), + }) + known = false + continue + } + + var err error + key, err = convert.Convert(key, cty.String) + if err != nil { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Incorrect key type", + Detail: fmt.Sprintf("Can't use this value as a key: %s.", err.Error()), + Subject: item.ValueExpr.Range().Ptr(), + }) + known = false + continue + } + + if !key.IsKnown() { + known = false + continue + } + + keyStr := key.AsString() + + vals[keyStr] = val + } + + if !known { + return cty.DynamicVal, diags + } + + return cty.ObjectVal(vals), diags +} + +func (e *ObjectConsExpr) Range() zcl.Range { + return e.SrcRange +} + +func (e *ObjectConsExpr) StartRange() zcl.Range { + return e.OpenRange +} diff --git a/zcl/zclsyntax/expression_test.go b/zcl/zclsyntax/expression_test.go index 3006113..cae5a05 100644 --- a/zcl/zclsyntax/expression_test.go +++ b/zcl/zclsyntax/expression_test.go @@ -179,6 +179,99 @@ upper( cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}), 0, }, + { + `{}`, + nil, + cty.EmptyObjectVal, + 0, + }, + { + `{"hello": "world"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{"hello" = "world"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{hello = "world"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{hello: "world"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{"hello" = "world", "goodbye" = "cruel world"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + "goodbye": cty.StringVal("cruel world"), + }), + 0, + }, + { + `{ + "hello" = "world" +}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{ + "hello" = "world" + "goodbye" = "cruel world" +}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + "goodbye": cty.StringVal("cruel world"), + }), + 0, + }, + { + `{ + "hello" = "world", + "goodbye" = "cruel world" +}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + "goodbye": cty.StringVal("cruel world"), + }), + 0, + }, + { + `{ + "hello" = "world", + "goodbye" = "cruel world", +}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + "goodbye": cty.StringVal("cruel world"), + }), + 0, + }, } for _, test := range tests { diff --git a/zcl/zclsyntax/expression_vars.go b/zcl/zclsyntax/expression_vars.go index d92f2c7..7b41c93 100755 --- a/zcl/zclsyntax/expression_vars.go +++ b/zcl/zclsyntax/expression_vars.go @@ -23,6 +23,10 @@ func (e *LiteralValueExpr) Variables() []zcl.Traversal { return Variables(e) } +func (e *ObjectConsExpr) Variables() []zcl.Traversal { + return Variables(e) +} + func (e *ScopeTraversalExpr) Variables() []zcl.Traversal { return Variables(e) } diff --git a/zcl/zclsyntax/parser.go b/zcl/zclsyntax/parser.go index afcaf50..2055542 100644 --- a/zcl/zclsyntax/parser.go +++ b/zcl/zclsyntax/parser.go @@ -590,6 +590,9 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) { case TokenOBrack: return p.parseTupleCons() + case TokenOBrace: + return p.parseObjectCons() + default: var diags zcl.Diagnostics if !p.recovery { @@ -752,6 +755,135 @@ func (p *parser) parseTupleCons() (Expression, zcl.Diagnostics) { }, diags } +func (p *parser) parseObjectCons() (Expression, zcl.Diagnostics) { + open := p.Read() + if open.Type != TokenOBrace { + // Should never happen if callers are behaving + panic("parseObjectCons called without peeker pointing to open brace") + } + + var close Token + + var diags zcl.Diagnostics + var items []ObjectConsItem + + p.PushIncludeNewlines(true) + defer p.PopIncludeNewlines() + + for { + next := p.Peek() + if next.Type == TokenNewline { + p.Read() // eat newline + continue + } + + if next.Type == TokenCBrace { + close = p.Read() // eat closer + break + } + + // As a special case, we allow the key to be a literal identifier. + // This means that a variable reference or function call can't appear + // directly as key expression, and must instead be wrapped in some + // disambiguation punctuation, like (var.a) = "b" or "${var.a}" = "b". + var key Expression + var keyDiags zcl.Diagnostics + if p.Peek().Type == TokenIdent { + nameTok := p.Read() + key = &LiteralValueExpr{ + Val: cty.StringVal(string(nameTok.Bytes)), + + SrcRange: nameTok.Range, + } + } else { + key, keyDiags = p.ParseExpression() + } + + diags = append(diags, keyDiags...) + + if p.recovery && keyDiags.HasErrors() { + // If expression parsing failed then we are probably in a strange + // place in the token stream, so we'll bail out and try to reset + // to after our closing brace to allow parsing to continue. + close = p.recover(TokenCBrace) + break + } + + next = p.Peek() + if next.Type != TokenEqual && next.Type != TokenColon { + if !p.recovery { + if next.Type == TokenNewline || next.Type == TokenComma { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Missing item value", + Detail: "Expected an item value, introduced by an equals sign (\"=\").", + Subject: &next.Range, + Context: zcl.RangeBetween(open.Range, next.Range).Ptr(), + }) + } else { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Missing key/value separator", + Detail: "Expected an equals sign (\"=\") to mark the beginning of the item value.", + Subject: &next.Range, + Context: zcl.RangeBetween(open.Range, next.Range).Ptr(), + }) + } + } + close = p.recover(TokenCBrace) + break + } + + p.Read() // eat equals sign or colon + + value, valueDiags := p.ParseExpression() + diags = append(diags, valueDiags...) + + if p.recovery && valueDiags.HasErrors() { + // If expression parsing failed then we are probably in a strange + // place in the token stream, so we'll bail out and try to reset + // to after our closing brace to allow parsing to continue. + close = p.recover(TokenCBrace) + break + } + + items = append(items, ObjectConsItem{ + KeyExpr: key, + ValueExpr: value, + }) + + next = p.Peek() + if next.Type == TokenCBrace { + close = p.Read() // eat closer + break + } + + if next.Type != TokenComma && next.Type != TokenNewline { + if !p.recovery { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Missing item separator", + Detail: "Expected a newline or comma to mark the beginning of the next item.", + Subject: &next.Range, + Context: zcl.RangeBetween(open.Range, next.Range).Ptr(), + }) + } + close = p.recover(TokenCBrace) + break + } + + p.Read() // eat comma or newline + + } + + return &ObjectConsExpr{ + Items: items, + + SrcRange: zcl.RangeBetween(open.Range, close.Range), + OpenRange: open.Range, + }, diags +} + func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) { var parts []Expression var diags zcl.Diagnostics