Skip to content

Commit fe9f503

Browse files
committed
cue: add fallback keyword for for comprehensions
Add support for the 'fallback' keyword in for comprehensions, distinct from 'else' used with if/try comprehensions: - 'for ... fallback { }' - when no iterations produce results - 'if ... else { }' - binary true/false choice - 'try ... else { }' - success/failure handling This provides clearer semantics: 'else' implies binary choice while 'fallback' indicates a default when no results are produced. Changes: - Add FALLBACK token to lexer - Rename ElseClause to FallbackClause in AST - Parser validates correct keyword per clause type - Both keywords can still be used as field labels - Update formatter and exporter for correct output - Gate both else and fallback clauses behind @experiment(try) - Undo spec changes to move them to the "holding" CL Signed-off-by: Marcel van Lohuizen <[email protected]> Change-Id: I3682486cd94f30bcfd3eba55b3f31dcdf3507542 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1231630 Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Daniel Martí <[email protected]> TryBot-Result: CUEcueckoo <[email protected]>
1 parent c76dc5a commit fe9f503

File tree

30 files changed

+686
-319
lines changed

30 files changed

+686
-319
lines changed

cue/ast/ast.go

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -389,9 +389,9 @@ func (a *PostfixAlias) End() token.Pos {
389389

390390
// A Comprehension node represents a comprehension declaration.
391391
type Comprehension struct {
392-
Clauses []Clause // There must be at least one clause.
393-
Value Expr // Must be a struct TODO: change to Struct
394-
Else *ElseClause // Optional else clause
392+
Clauses []Clause // There must be at least one clause.
393+
Value Expr // Must be a struct TODO: change to Struct
394+
Fallback *FallbackClause // Optional else/fallback clause
395395

396396
comments
397397
decl
@@ -401,8 +401,8 @@ type Comprehension struct {
401401
func (x *Comprehension) Pos() token.Pos { return getPos(x) }
402402
func (x *Comprehension) pos() *token.Pos { return x.Clauses[0].pos() }
403403
func (x *Comprehension) End() token.Pos {
404-
if x.Else != nil {
405-
return x.Else.Body.End()
404+
if x.Fallback != nil {
405+
return x.Fallback.Body.End()
406406
}
407407
return x.Value.End()
408408
}
@@ -723,10 +723,14 @@ type LetClause struct {
723723
decl
724724
}
725725

726-
// An ElseClause node represents an else clause in a comprehension.
727-
type ElseClause struct {
728-
Else token.Pos
729-
Body *StructLit
726+
// A FallbackClause node represents an else or fallback clause in a comprehension.
727+
// Used with `else` after if/try clauses, and `fallback` after for clauses.
728+
type FallbackClause struct {
729+
// TODO: note that the support for "else" is likely temporary, as
730+
// we will move that functionality to an "if" and "try" element with an
731+
// optional "else" body.
732+
Fallback token.Pos // Position of "else" or "fallback" keyword
733+
Body *StructLit
730734

731735
comments
732736
clause
@@ -882,38 +886,38 @@ func (x *StructLit) pos() *token.Pos {
882886
return &x.Lbrace
883887
}
884888

885-
func (x *ListLit) Pos() token.Pos { return x.Lbrack }
886-
func (x *ListLit) pos() *token.Pos { return &x.Lbrack }
887-
func (x *Ellipsis) Pos() token.Pos { return x.Ellipsis }
888-
func (x *Ellipsis) pos() *token.Pos { return &x.Ellipsis }
889-
func (x *LetClause) Pos() token.Pos { return x.Let }
890-
func (x *LetClause) pos() *token.Pos { return &x.Let }
891-
func (x *TryClause) Pos() token.Pos { return x.Try }
892-
func (x *TryClause) pos() *token.Pos { return &x.Try }
893-
func (x *ForClause) Pos() token.Pos { return x.For }
894-
func (x *ForClause) pos() *token.Pos { return &x.For }
895-
func (x *IfClause) Pos() token.Pos { return x.If }
896-
func (x *IfClause) pos() *token.Pos { return &x.If }
897-
func (x *ElseClause) Pos() token.Pos { return x.Else }
898-
func (x *ElseClause) pos() *token.Pos { return &x.Else }
899-
func (x *ParenExpr) Pos() token.Pos { return x.Lparen }
900-
func (x *ParenExpr) pos() *token.Pos { return &x.Lparen }
901-
func (x *SelectorExpr) Pos() token.Pos { return x.X.Pos() }
902-
func (x *SelectorExpr) pos() *token.Pos { return x.X.pos() }
903-
func (x *IndexExpr) Pos() token.Pos { return x.X.Pos() }
904-
func (x *IndexExpr) pos() *token.Pos { return x.X.pos() }
905-
func (x *SliceExpr) Pos() token.Pos { return x.X.Pos() }
906-
func (x *SliceExpr) pos() *token.Pos { return x.X.pos() }
907-
func (x *CallExpr) Pos() token.Pos { return x.Fun.Pos() }
908-
func (x *CallExpr) pos() *token.Pos { return x.Fun.pos() }
909-
func (x *UnaryExpr) Pos() token.Pos { return x.OpPos }
910-
func (x *UnaryExpr) pos() *token.Pos { return &x.OpPos }
911-
func (x *BinaryExpr) Pos() token.Pos { return x.X.Pos() }
912-
func (x *BinaryExpr) pos() *token.Pos { return x.X.pos() }
913-
func (x *PostfixExpr) Pos() token.Pos { return x.X.Pos() }
914-
func (x *PostfixExpr) pos() *token.Pos { return x.X.pos() }
915-
func (x *BottomLit) Pos() token.Pos { return x.Bottom }
916-
func (x *BottomLit) pos() *token.Pos { return &x.Bottom }
889+
func (x *ListLit) Pos() token.Pos { return x.Lbrack }
890+
func (x *ListLit) pos() *token.Pos { return &x.Lbrack }
891+
func (x *Ellipsis) Pos() token.Pos { return x.Ellipsis }
892+
func (x *Ellipsis) pos() *token.Pos { return &x.Ellipsis }
893+
func (x *LetClause) Pos() token.Pos { return x.Let }
894+
func (x *LetClause) pos() *token.Pos { return &x.Let }
895+
func (x *TryClause) Pos() token.Pos { return x.Try }
896+
func (x *TryClause) pos() *token.Pos { return &x.Try }
897+
func (x *ForClause) Pos() token.Pos { return x.For }
898+
func (x *ForClause) pos() *token.Pos { return &x.For }
899+
func (x *IfClause) Pos() token.Pos { return x.If }
900+
func (x *IfClause) pos() *token.Pos { return &x.If }
901+
func (x *FallbackClause) Pos() token.Pos { return x.Fallback }
902+
func (x *FallbackClause) pos() *token.Pos { return &x.Fallback }
903+
func (x *ParenExpr) Pos() token.Pos { return x.Lparen }
904+
func (x *ParenExpr) pos() *token.Pos { return &x.Lparen }
905+
func (x *SelectorExpr) Pos() token.Pos { return x.X.Pos() }
906+
func (x *SelectorExpr) pos() *token.Pos { return x.X.pos() }
907+
func (x *IndexExpr) Pos() token.Pos { return x.X.Pos() }
908+
func (x *IndexExpr) pos() *token.Pos { return x.X.pos() }
909+
func (x *SliceExpr) Pos() token.Pos { return x.X.Pos() }
910+
func (x *SliceExpr) pos() *token.Pos { return x.X.pos() }
911+
func (x *CallExpr) Pos() token.Pos { return x.Fun.Pos() }
912+
func (x *CallExpr) pos() *token.Pos { return x.Fun.pos() }
913+
func (x *UnaryExpr) Pos() token.Pos { return x.OpPos }
914+
func (x *UnaryExpr) pos() *token.Pos { return &x.OpPos }
915+
func (x *BinaryExpr) Pos() token.Pos { return x.X.Pos() }
916+
func (x *BinaryExpr) pos() *token.Pos { return x.X.pos() }
917+
func (x *PostfixExpr) Pos() token.Pos { return x.X.Pos() }
918+
func (x *PostfixExpr) pos() *token.Pos { return x.X.pos() }
919+
func (x *BottomLit) Pos() token.Pos { return x.Bottom }
920+
func (x *BottomLit) pos() *token.Pos { return &x.Bottom }
917921

918922
func (x *BadExpr) End() token.Pos { return x.To }
919923
func (x *Ident) End() token.Pos {
@@ -943,16 +947,16 @@ func (x *TryClause) End() token.Pos {
943947
}
944948
return x.Try.Add(3) // len("try")
945949
}
946-
func (x *ForClause) End() token.Pos { return x.Source.End() }
947-
func (x *IfClause) End() token.Pos { return x.Condition.End() }
948-
func (x *ElseClause) End() token.Pos { return x.Body.End() }
949-
func (x *ParenExpr) End() token.Pos { return x.Rparen.Add(1) }
950-
func (x *SelectorExpr) End() token.Pos { return x.Sel.End() }
951-
func (x *IndexExpr) End() token.Pos { return x.Rbrack.Add(1) }
952-
func (x *SliceExpr) End() token.Pos { return x.Rbrack.Add(1) }
953-
func (x *CallExpr) End() token.Pos { return x.Rparen.Add(1) }
954-
func (x *UnaryExpr) End() token.Pos { return x.X.End() }
955-
func (x *BinaryExpr) End() token.Pos { return x.Y.End() }
950+
func (x *ForClause) End() token.Pos { return x.Source.End() }
951+
func (x *IfClause) End() token.Pos { return x.Condition.End() }
952+
func (x *FallbackClause) End() token.Pos { return x.Body.End() }
953+
func (x *ParenExpr) End() token.Pos { return x.Rparen.Add(1) }
954+
func (x *SelectorExpr) End() token.Pos { return x.Sel.End() }
955+
func (x *IndexExpr) End() token.Pos { return x.Rbrack.Add(1) }
956+
func (x *SliceExpr) End() token.Pos { return x.Rbrack.Add(1) }
957+
func (x *CallExpr) End() token.Pos { return x.Rparen.Add(1) }
958+
func (x *UnaryExpr) End() token.Pos { return x.X.End() }
959+
func (x *BinaryExpr) End() token.Pos { return x.Y.End() }
956960
func (x *PostfixExpr) End() token.Pos {
957961
switch x.Op {
958962
case token.ELLIPSIS:

cue/ast/astutil/apply.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ func applyCursor(v applyVisitor, c Cursor) {
476476
case *ast.Comprehension:
477477
applyList(v, c, n.Clauses)
478478
apply(v, c, &n.Value)
479-
applyIfNotNil(v, c, &n.Else)
479+
applyIfNotNil(v, c, &n.Fallback)
480480

481481
// Files and packages
482482
case *ast.File:
@@ -493,7 +493,7 @@ func applyCursor(v applyVisitor, c Cursor) {
493493
case *ast.IfClause:
494494
apply(v, c, &n.Condition)
495495

496-
case *ast.ElseClause:
496+
case *ast.FallbackClause:
497497
apply(v, c, &n.Body)
498498

499499
default:

cue/ast/astutil/resolve.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,10 +380,10 @@ func (s *scope) Before(n ast.Node) bool {
380380
s = scopeClauses(s, x.Clauses)
381381
defer s.freeScopesUntil(outer)
382382
ast.Walk(x.Value, s.Before, nil)
383-
// Walk the else clause in the OUTER scope, since else should not
383+
// Walk the fallback clause in the OUTER scope, since fallback should not
384384
// have access to for/let variables from the comprehension clauses.
385-
if x.Else != nil {
386-
ast.Walk(x.Else.Body, outer.Before, nil)
385+
if x.Fallback != nil {
386+
ast.Walk(x.Fallback.Body, outer.Before, nil)
387387
}
388388
return false
389389

cue/ast/walk.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func Walk(node Node, before func(Node) bool, after func(Node)) {
156156
case *Comprehension:
157157
walkList(n.Clauses, before, after)
158158
Walk(n.Value, before, after)
159-
walkIfNotNil(n.Else, before, after)
159+
walkIfNotNil(n.Fallback, before, after)
160160

161161
// Files and packages
162162
case *File:
@@ -173,7 +173,7 @@ func Walk(node Node, before func(Node) bool, after func(Node)) {
173173
case *IfClause:
174174
Walk(n.Condition, before, after)
175175

176-
case *ElseClause:
176+
case *FallbackClause:
177177
Walk(n.Body, before, after)
178178

179179
default:

cue/format/node.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,16 @@ func (f *formatter) walkListElems(list []ast.Expr) {
251251
f.walkClauseList(n.Clauses, blank)
252252
f.print(blank, nooverride)
253253
f.expr(n.Value)
254-
if n.Else != nil {
255-
f.print(blank, n.Else.Else, token.ELSE, blank)
256-
f.expr(n.Else.Body)
254+
if n.Fallback != nil {
255+
// Use FALLBACK keyword for 'for' comprehensions, ELSE for 'if'/'try'
256+
kw := token.ELSE
257+
if len(n.Clauses) > 0 {
258+
if _, ok := n.Clauses[0].(*ast.ForClause); ok {
259+
kw = token.FALLBACK
260+
}
261+
}
262+
f.print(blank, n.Fallback.Fallback, kw, blank)
263+
f.expr(n.Fallback.Body)
257264
}
258265

259266
case *ast.Ellipsis:
@@ -497,9 +504,16 @@ func (f *formatter) embedding(decl ast.Expr) {
497504
f.walkClauseList(n.Clauses, blank)
498505
f.print(blank, nooverride)
499506
f.expr(n.Value)
500-
if n.Else != nil {
501-
f.print(blank, n.Else.Else, token.ELSE, blank)
502-
f.expr(n.Else.Body)
507+
if n.Fallback != nil {
508+
// Use FALLBACK keyword for 'for' comprehensions, ELSE for 'if'/'try'
509+
kw := token.ELSE
510+
if len(n.Clauses) > 0 {
511+
if _, ok := n.Clauses[0].(*ast.ForClause); ok {
512+
kw = token.FALLBACK
513+
}
514+
}
515+
f.print(blank, n.Fallback.Fallback, kw, blank)
516+
f.expr(n.Fallback.Body)
503517
}
504518

505519
case *ast.Ellipsis:

cue/parser/parser.go

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -866,19 +866,22 @@ func (p *parser) parseComprehension() (decl ast.Decl, ident *ast.Ident) {
866866
expr := p.parseStruct()
867867
sc.closeExpr(p, expr)
868868

869-
var elseClause *ast.ElseClause
870-
if p.tok == token.ELSE {
871-
elseClause = p.parseElseClause()
869+
// Determine if this comprehension starts with a for clause
870+
hasForClause := tok == token.FOR
871+
872+
var fallbackClause *ast.FallbackClause
873+
if p.tok == token.ELSE || p.tok == token.FALLBACK {
874+
fallbackClause = p.parseFallbackClause(hasForClause)
872875
}
873876

874877
if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF
875878
p.next()
876879
}
877880

878881
return &ast.Comprehension{
879-
Clauses: clauses,
880-
Value: expr,
881-
Else: elseClause,
882+
Clauses: clauses,
883+
Value: expr,
884+
Fallback: fallbackClause,
882885
}, nil
883886
}
884887

@@ -950,7 +953,8 @@ func (p *parser) parseField() (decl ast.Decl) {
950953
case token.IDENT, token.LBRACK, token.LPAREN,
951954
token.STRING, token.INTERPOLATION,
952955
token.NULL, token.TRUE, token.FALSE,
953-
token.FOR, token.IF, token.LET, token.IN:
956+
token.FOR, token.IF, token.LET, token.IN,
957+
token.TRY, token.ELSE, token.FALLBACK:
954958
return &ast.EmbedDecl{Expr: expr}
955959
}
956960
fallthrough
@@ -1041,6 +1045,10 @@ func (p *parser) parseLabel(rhs bool) (label ast.Label, expr ast.Expr, decl ast.
10411045
}
10421046
expr = ident
10431047

1048+
case token.ELSE, token.FALLBACK:
1049+
// These keywords can be used as field labels
1050+
expr = p.parseExpr()
1051+
10441052
case token.LET:
10451053
let, ident := p.parseLetDecl()
10461054
if let != nil {
@@ -1231,18 +1239,37 @@ func (p *parser) parseComprehensionClauses() (clauses []ast.Clause, c *commentSt
12311239
}
12321240
}
12331241

1234-
// parseElseClause parses an else clause in a comprehension.
1235-
func (p *parser) parseElseClause() *ast.ElseClause {
1242+
// parseFallbackClause parses an else or fallback clause in a comprehension.
1243+
// It accepts the appropriate keyword based on hasForClause:
1244+
// - hasForClause=true: expects FALLBACK, errors on ELSE
1245+
// - hasForClause=false: expects ELSE, errors on FALLBACK
1246+
func (p *parser) parseFallbackClause(hasForClause bool) *ast.FallbackClause {
12361247
if p.trace {
1237-
defer un(trace(p, "ElseClause"))
1248+
defer un(trace(p, "FallbackClause"))
12381249
}
12391250
c := p.openComments()
1240-
elsePos := p.expect(token.ELSE)
1251+
1252+
if p.experiments == nil || !p.experiments.Try {
1253+
p.errf(p.pos, "%s requires @experiment(try)", p.tok)
1254+
}
1255+
1256+
var pos token.Pos
1257+
if hasForClause {
1258+
if p.tok == token.ELSE {
1259+
p.errf(p.pos, "use 'fallback' with 'for' clauses")
1260+
}
1261+
pos = p.expect(token.FALLBACK)
1262+
} else {
1263+
if p.tok == token.FALLBACK {
1264+
p.errf(p.pos, "use 'else' with 'if' clauses")
1265+
}
1266+
pos = p.expect(token.ELSE)
1267+
}
12411268
body := p.parseStruct()
1242-
return c.closeClause(p, &ast.ElseClause{
1243-
Else: elsePos,
1244-
Body: body.(*ast.StructLit),
1245-
}).(*ast.ElseClause)
1269+
return c.closeClause(p, &ast.FallbackClause{
1270+
Fallback: pos,
1271+
Body: body.(*ast.StructLit),
1272+
}).(*ast.FallbackClause)
12461273
}
12471274

12481275
func (p *parser) parseFunc() (expr ast.Expr) {
@@ -1366,19 +1393,22 @@ func (p *parser) parseListElement() (expr ast.Expr, ok bool) {
13661393
expr := p.parseStruct()
13671394
sc.closeExpr(p, expr)
13681395

1369-
var elseClause *ast.ElseClause
1370-
if p.tok == token.ELSE {
1371-
elseClause = p.parseElseClause()
1396+
// Determine if this comprehension starts with a for clause
1397+
hasForClause := tok == token.FOR
1398+
1399+
var fallbackClause *ast.FallbackClause
1400+
if p.tok == token.ELSE || p.tok == token.FALLBACK {
1401+
fallbackClause = p.parseFallbackClause(hasForClause)
13721402
}
13731403

13741404
if p.atComma("list literal", token.RBRACK) { // TODO: may be EOF
13751405
p.next()
13761406
}
13771407

13781408
return &ast.Comprehension{
1379-
Clauses: clauses,
1380-
Value: expr,
1381-
Else: elseClause,
1409+
Clauses: clauses,
1410+
Value: expr,
1411+
Fallback: fallbackClause,
13821412
}, true
13831413
}
13841414

0 commit comments

Comments
 (0)