diff --git a/zcl/zclsyntax/expression_template_test.go b/zcl/zclsyntax/expression_template_test.go index cf2be24..64274a7 100644 --- a/zcl/zclsyntax/expression_template_test.go +++ b/zcl/zclsyntax/expression_template_test.go @@ -128,6 +128,49 @@ trim`, cty.StringVal("truetrimtrue"), // trimming is no-op of neighbors aren't literal strings 0, }, + + { + `!{ if true ~} hello !{~ endif }`, + nil, + cty.StringVal("hello"), + 0, + }, + { + `!{ if false ~} hello !{~ endif}`, + nil, + cty.StringVal(""), + 0, + }, + { + `!{ if true ~} hello !{~ else ~} goodbye !{~ endif }`, + nil, + cty.StringVal("hello"), + 0, + }, + { + `!{ if false ~} hello !{~ else ~} goodbye !{~ endif }`, + nil, + cty.StringVal("goodbye"), + 0, + }, + { + `!{ if true ~} !{~ if false ~} hello !{~ else ~} goodbye !{~ endif ~} !{~ endif }`, + nil, + cty.StringVal("goodbye"), + 0, + }, + { + `!{ if false ~} !{~ if false ~} hello !{~ else ~} goodbye !{~ endif ~} !{~ endif }`, + nil, + cty.StringVal(""), + 0, + }, + { + `!{ of true ~} hello !{~ endif}`, + nil, + cty.UnknownVal(cty.String), + 2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected + }, } for _, test := range tests { diff --git a/zcl/zclsyntax/keywords.go b/zcl/zclsyntax/keywords.go index 83e1109..03af9f3 100644 --- a/zcl/zclsyntax/keywords.go +++ b/zcl/zclsyntax/keywords.go @@ -9,6 +9,9 @@ type Keyword []byte var forKeyword = Keyword([]byte{'f', 'o', 'r'}) var inKeyword = Keyword([]byte{'i', 'n'}) var ifKeyword = Keyword([]byte{'i', 'f'}) +var elseKeyword = Keyword([]byte{'e', 'l', 's', 'e'}) +var endifKeyword = Keyword([]byte{'e', 'n', 'd', 'i', 'f'}) +var endforKeyword = Keyword([]byte{'e', 'n', 'd', 'f', 'o', 'r'}) func (kw Keyword) TokenMatches(token Token) bool { if token.Type != TokenIdent { diff --git a/zcl/zclsyntax/parser_template.go b/zcl/zclsyntax/parser_template.go index 8f84176..c115d38 100644 --- a/zcl/zclsyntax/parser_template.go +++ b/zcl/zclsyntax/parser_template.go @@ -69,27 +69,29 @@ func (p *templateParser) parseRoot() ([]Expression, zcl.Diagnostics) { } func (p *templateParser) parseExpr() (Expression, zcl.Diagnostics) { - next := p.Read() + next := p.Peek() switch tok := next.(type) { case *templateLiteralToken: + p.Read() // eat literal return &LiteralValueExpr{ Val: cty.StringVal(tok.Val), SrcRange: tok.SrcRange, }, nil case *templateInterpToken: + p.Read() // eat interp return tok.Expr, nil case *templateIfToken: - // TODO: implement - panic("template if token not yet implemented") + return p.parseIf() case *templateForToken: // TODO: implement panic("template for token not yet implemented") case *templateEndToken: + p.Read() // eat erroneous token return errPlaceholderExpr(tok.SrcRange), zcl.Diagnostics{ { // This is a particularly unhelpful diagnostic, so callers @@ -103,6 +105,7 @@ func (p *templateParser) parseExpr() (Expression, zcl.Diagnostics) { } case *templateEndCtrlToken: + p.Read() // eat erroneous token return errPlaceholderExpr(tok.SrcRange), zcl.Diagnostics{ { Severity: zcl.DiagError, @@ -118,6 +121,118 @@ func (p *templateParser) parseExpr() (Expression, zcl.Diagnostics) { } } +func (p *templateParser) parseIf() (Expression, zcl.Diagnostics) { + open := p.Read() + openIf, isIf := open.(*templateIfToken) + if !isIf { + // should never happen if caller is behaving + panic("parseIf called with peeker not pointing at if token") + } + + var ifExprs, elseExprs []Expression + var diags zcl.Diagnostics + var endifRange zcl.Range + + currentExprs := &ifExprs +Token: + for { + next := p.Peek() + if end, isEnd := next.(*templateEndToken); isEnd { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unexpected end of template", + Detail: fmt.Sprintf( + "The if directive at %s is missing its corresponding endif directive.", + openIf.SrcRange, + ), + Subject: &end.SrcRange, + }) + return errPlaceholderExpr(end.SrcRange), diags + } + if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd { + p.Read() // eat end directive + + switch end.Type { + + case templateElse: + if currentExprs == &ifExprs { + currentExprs = &elseExprs + continue Token + } + + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unexpected else directive", + Detail: fmt.Sprintf( + "Already in the else clause for the if started at %s.", + openIf.SrcRange, + ), + Subject: &end.SrcRange, + }) + + case templateEndIf: + endifRange = end.SrcRange + break Token + + default: + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: fmt.Sprintf("Unexpected %s directive", end.Name()), + Detail: fmt.Sprintf( + "Expecting an endif directive for the if started at %s.", + openIf.SrcRange, + ), + Subject: &end.SrcRange, + }) + } + + return errPlaceholderExpr(end.SrcRange), diags + } + + expr, exprDiags := p.parseExpr() + diags = append(diags, exprDiags...) + *currentExprs = append(*currentExprs, expr) + } + + if len(ifExprs) == 0 { + ifExprs = append(ifExprs, &LiteralValueExpr{ + Val: cty.StringVal(""), + SrcRange: zcl.Range{ + Filename: openIf.SrcRange.Filename, + Start: openIf.SrcRange.End, + End: openIf.SrcRange.End, + }, + }) + } + if len(elseExprs) == 0 { + elseExprs = append(elseExprs, &LiteralValueExpr{ + Val: cty.StringVal(""), + SrcRange: zcl.Range{ + Filename: endifRange.Filename, + Start: endifRange.Start, + End: endifRange.Start, + }, + }) + } + + trueExpr := &TemplateExpr{ + Parts: ifExprs, + SrcRange: zcl.RangeBetween(ifExprs[0].Range(), ifExprs[len(ifExprs)-1].Range()), + } + falseExpr := &TemplateExpr{ + Parts: elseExprs, + SrcRange: zcl.RangeBetween(elseExprs[0].Range(), elseExprs[len(elseExprs)-1].Range()), + } + + return &ConditionalExpr{ + Condition: openIf.CondExpr, + TrueResult: trueExpr, + FalseResult: falseExpr, + + SrcRange: zcl.RangeBetween(openIf.SrcRange, endifRange), + }, diags +} + func (p *templateParser) Peek() templateToken { return p.Tokens[p.pos] } @@ -214,8 +329,104 @@ Token: Expr: expr, SrcRange: zcl.RangeBetween(next.Range, close.Range), }) + case TokenTemplateControl: - panic("template control sequences not yet supported") + // if the opener is !{~ then we want to eat any trailing whitespace + // in the preceding literal token, assuming it is indeed a literal + // token. + if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 { + prevExpr := parts[len(parts)-1] + if lexpr, ok := prevExpr.(*templateLiteralToken); ok { + lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace) + } + } + p.PushIncludeNewlines(false) + + kw := p.Peek() + if kw.Type != TokenIdent { + if !p.recovery { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid template control keyword", + Detail: "A template control keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.", + Subject: &kw.Range, + Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(), + }) + } + p.recover(TokenTemplateSeqEnd) + p.PopIncludeNewlines() + continue Token + } + p.Read() // eat keyword token + + switch { + + case ifKeyword.TokenMatches(kw): + condExpr, exprDiags := p.ParseExpression() + diags = append(diags, exprDiags...) + parts = append(parts, &templateIfToken{ + CondExpr: condExpr, + SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), + }) + + case elseKeyword.TokenMatches(kw): + parts = append(parts, &templateEndCtrlToken{ + Type: templateElse, + SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), + }) + + case endifKeyword.TokenMatches(kw): + parts = append(parts, &templateEndCtrlToken{ + Type: templateEndIf, + SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), + }) + + default: + if !p.recovery { + suggestions := []string{"if", "for", "else", "endif", "endfor"} + given := string(kw.Bytes) + suggestion := nameSuggestion(given, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid template control keyword", + Detail: fmt.Sprintf("%q is not a valid template control keyword.%s", given, suggestion), + Subject: &kw.Range, + Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(), + }) + } + p.recover(TokenTemplateSeqEnd) + p.PopIncludeNewlines() + continue Token + + } + + close := p.Peek() + if close.Type != TokenTemplateSeqEnd { + if !p.recovery { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: fmt.Sprintf("Extra characters in %s marker", kw.Bytes), + Detail: "Expected a closing brace to end the sequence, but found extra characters.", + Subject: &close.Range, + Context: zcl.RangeBetween(startRange, close.Range).Ptr(), + }) + } + p.recover(TokenTemplateSeqEnd) + } else { + p.Read() // eat closing brace + + // If the closer is ~} then we want to eat any leading + // whitespace on the next token, if it turns out to be a + // literal token. + if len(close.Bytes) == 2 && close.Bytes[0] == '~' { + ltrimNext = true + } + } + p.PopIncludeNewlines() default: if !p.recovery {