this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 <mpvl@gmail.com>
Change-Id: I3682486cd94f30bcfd3eba55b3f31dcdf3507542
Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1231630
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+685 -318
+55 -51
cue/ast/ast.go
··· 389 389 390 390 // A Comprehension node represents a comprehension declaration. 391 391 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 395 395 396 396 comments 397 397 decl ··· 401 401 func (x *Comprehension) Pos() token.Pos { return getPos(x) } 402 402 func (x *Comprehension) pos() *token.Pos { return x.Clauses[0].pos() } 403 403 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() 406 406 } 407 407 return x.Value.End() 408 408 } ··· 723 723 decl 724 724 } 725 725 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 730 734 731 735 comments 732 736 clause ··· 882 886 return &x.Lbrace 883 887 } 884 888 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 } 917 921 918 922 func (x *BadExpr) End() token.Pos { return x.To } 919 923 func (x *Ident) End() token.Pos { ··· 943 947 } 944 948 return x.Try.Add(3) // len("try") 945 949 } 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() } 956 960 func (x *PostfixExpr) End() token.Pos { 957 961 switch x.Op { 958 962 case token.ELLIPSIS:
+2 -2
cue/ast/astutil/apply.go
··· 476 476 case *ast.Comprehension: 477 477 applyList(v, c, n.Clauses) 478 478 apply(v, c, &n.Value) 479 - applyIfNotNil(v, c, &n.Else) 479 + applyIfNotNil(v, c, &n.Fallback) 480 480 481 481 // Files and packages 482 482 case *ast.File: ··· 493 493 case *ast.IfClause: 494 494 apply(v, c, &n.Condition) 495 495 496 - case *ast.ElseClause: 496 + case *ast.FallbackClause: 497 497 apply(v, c, &n.Body) 498 498 499 499 default:
+3 -3
cue/ast/astutil/resolve.go
··· 380 380 s = scopeClauses(s, x.Clauses) 381 381 defer s.freeScopesUntil(outer) 382 382 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 384 384 // 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) 387 387 } 388 388 return false 389 389
+2 -2
cue/ast/walk.go
··· 156 156 case *Comprehension: 157 157 walkList(n.Clauses, before, after) 158 158 Walk(n.Value, before, after) 159 - walkIfNotNil(n.Else, before, after) 159 + walkIfNotNil(n.Fallback, before, after) 160 160 161 161 // Files and packages 162 162 case *File: ··· 173 173 case *IfClause: 174 174 Walk(n.Condition, before, after) 175 175 176 - case *ElseClause: 176 + case *FallbackClause: 177 177 Walk(n.Body, before, after) 178 178 179 179 default:
+20 -6
cue/format/node.go
··· 251 251 f.walkClauseList(n.Clauses, blank) 252 252 f.print(blank, nooverride) 253 253 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) 257 264 } 258 265 259 266 case *ast.Ellipsis: ··· 497 504 f.walkClauseList(n.Clauses, blank) 498 505 f.print(blank, nooverride) 499 506 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) 503 517 } 504 518 505 519 case *ast.Ellipsis:
+51 -21
cue/parser/parser.go
··· 866 866 expr := p.parseStruct() 867 867 sc.closeExpr(p, expr) 868 868 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) 872 875 } 873 876 874 877 if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF ··· 876 879 } 877 880 878 881 return &ast.Comprehension{ 879 - Clauses: clauses, 880 - Value: expr, 881 - Else: elseClause, 882 + Clauses: clauses, 883 + Value: expr, 884 + Fallback: fallbackClause, 882 885 }, nil 883 886 } 884 887 ··· 950 953 case token.IDENT, token.LBRACK, token.LPAREN, 951 954 token.STRING, token.INTERPOLATION, 952 955 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: 954 958 return &ast.EmbedDecl{Expr: expr} 955 959 } 956 960 fallthrough ··· 1041 1045 } 1042 1046 expr = ident 1043 1047 1048 + case token.ELSE, token.FALLBACK: 1049 + // These keywords can be used as field labels 1050 + expr = p.parseExpr() 1051 + 1044 1052 case token.LET: 1045 1053 let, ident := p.parseLetDecl() 1046 1054 if let != nil { ··· 1231 1239 } 1232 1240 } 1233 1241 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 { 1236 1247 if p.trace { 1237 - defer un(trace(p, "ElseClause")) 1248 + defer un(trace(p, "FallbackClause")) 1238 1249 } 1239 1250 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 + } 1241 1268 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) 1246 1273 } 1247 1274 1248 1275 func (p *parser) parseFunc() (expr ast.Expr) { ··· 1366 1393 expr := p.parseStruct() 1367 1394 sc.closeExpr(p, expr) 1368 1395 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) 1372 1402 } 1373 1403 1374 1404 if p.atComma("list literal", token.RBRACK) { // TODO: may be EOF ··· 1376 1406 } 1377 1407 1378 1408 return &ast.Comprehension{ 1379 - Clauses: clauses, 1380 - Value: expr, 1381 - Else: elseClause, 1409 + Clauses: clauses, 1410 + Value: expr, 1411 + Fallback: fallbackClause, 1382 1412 }, true 1383 1413 } 1384 1414
+44 -17
cue/parser/parser_test.go
··· 548 548 { 549 549 desc: "if-else comprehension", 550 550 version: "v0.16.0", 551 - in: `{ 551 + in: `@experiment(try) 552 + { 552 553 a: { 553 554 if true { x: 1 } else { y: 2 } 554 555 } 555 556 }`, 556 - out: `{a: {if true {x: 1} else {y: 2}}}`, 557 + out: `@experiment(try), {a: {if true {x: 1} else {y: 2}}}`, 557 558 }, 558 559 { 559 - desc: "for-else comprehension", 560 + desc: "for-fallback comprehension", 560 561 version: "v0.16.0", 561 - in: `{ 562 + in: `@experiment(try) 563 + { 562 564 a: { 563 - for x in [] { "\(x)": x } else { empty: true } 565 + for x in [] { "\(x)": x } fallback { empty: true } 564 566 } 565 567 }`, 566 - out: `{a: {for x in [] {"\(x)": x} else {empty: true}}}`, 568 + out: `@experiment(try), {a: {for x in [] {"\(x)": x} fallback {empty: true}}}`, 567 569 }, 568 570 { 569 - desc: "multi-clause comprehension with else", 571 + desc: "multi-clause comprehension with fallback", 570 572 version: "v0.16.0", 571 - in: `{ 573 + in: `@experiment(try) 574 + { 572 575 a: { 573 - for x in [1,2] if x > 10 { "\(x)": x } else { none: true } 576 + for x in [1,2] if x > 10 { "\(x)": x } fallback { none: true } 574 577 } 575 578 }`, 576 - out: `{a: {for x in [1, 2] if x>10 {"\(x)": x} else {none: true}}}`, 579 + out: `@experiment(try), {a: {for x in [1, 2] if x>10 {"\(x)": x} fallback {none: true}}}`, 577 580 }, 578 581 { 579 - desc: "list comprehension with else", 582 + desc: "list comprehension with fallback", 580 583 version: "v0.16.0", 581 - in: `{ 582 - a: [for x in [] { x } else { 0 }] 584 + in: `@experiment(try) 585 + { 586 + a: [for x in [] { x } fallback { 0 }] 583 587 }`, 584 - out: `{a: [for x in [] {x} else {0}]}`, 588 + out: `@experiment(try), {a: [for x in [] {x} fallback {0}]}`, 585 589 }, 586 590 { 587 591 desc: "try struct form", ··· 658 662 { 659 663 desc: "multiple else clauses error", 660 664 version: "v0.16.0", 665 + in: `@experiment(try) 666 + 667 + a: { 668 + if true { x: 1 } else { y: 2 } else { z: 3 } 669 + } 670 + }`, 671 + out: `@experiment(try), a: {if true {x: 1} else {y: 2}, {z: 3}} 672 + missing ',' in struct literal (and 1 more errors)`, 673 + }, 674 + { 675 + desc: "else requires experiment even with latest version", 676 + version: "v1000.0.0", // Future version that should still require the experiment 661 677 in: `{ 662 678 a: { 663 - if true { x: 1 } else { y: 2 } else { z: 3 } 679 + if true { x: 1 } else { y: 2 } 680 + } 681 + }`, 682 + out: `{a: {if true {x: 1} else {y: 2}}} 683 + else requires @experiment(try)`, 684 + }, 685 + { 686 + desc: "fallback requires experiment even with latest version", 687 + version: "v1000.0.0", // Future version that should still require the experiment 688 + in: `{ 689 + a: { 690 + for x in [] { "\(x)": x } fallback { empty: true } 664 691 } 665 692 }`, 666 - out: `{a: {if true {x: 1} else {y: 2}, {z: 3}}} 667 - missing ',' in struct literal`, 693 + out: `{a: {for x in [] {"\(x)": x} fallback {empty: true}}} 694 + fallback requires @experiment(try)`, 668 695 }, 669 696 { 670 697 desc: "let declaration",
+26 -24
cue/testdata/comprehensions/else.txtar
··· 1 1 -- in.cue -- 2 + @experiment(try) 3 + 2 4 // if-else: condition true - no else 3 5 ifTrue: { 4 6 if true { a: 1 } else { b: 2 } ··· 11 13 12 14 // for-else: non-empty source - no else 13 15 forNonEmpty: { 14 - for x in [1, 2] { "\(x)": x } else { empty: true } 16 + for x in [1, 2] { "\(x)": x } fallback { empty: true } 15 17 } 16 18 17 19 // for-else: empty source - else yields 18 20 forEmpty: { 19 - for x in [] { "\(x)": x } else { empty: true } 21 + for x in [] { "\(x)": x } fallback { empty: true } 20 22 } 21 23 22 24 // for-else with filter: filter removes all - else yields 23 25 forFilteredAll: { 24 - for x in [1, 2, 3] if x > 10 { "\(x)": x } else { empty: true } 26 + for x in [1, 2, 3] if x > 10 { "\(x)": x } fallback { empty: true } 25 27 } 26 28 27 29 // for-else with filter: filter keeps some - no else 28 30 forFilteredSome: { 29 - for x in [1, 2, 3] if x > 1 { "\(x)": x } else { empty: true } 31 + for x in [1, 2, 3] if x > 1 { "\(x)": x } fallback { empty: true } 30 32 } 31 33 32 34 // list if-else: true ··· 41 43 42 44 // list for-else: empty 43 45 listForEmpty: [ 44 - for x in [] { x } else { 0 } 46 + for x in [] { x } fallback { 0 } 45 47 ] 46 48 47 49 // list for-else: non-empty 48 50 listForNonEmpty: [ 49 - for x in [1, 2] { x * 2 } else { 0 } 51 + for x in [1, 2] { x * 2 } fallback { 0 } 50 52 ] 51 53 52 54 // else with multiple fields ··· 60 62 if false { added: 2 } else { fallback: 3 } 61 63 } 62 64 63 - // else with outer scope access 65 + // fallback with outer scope access 64 66 outerScope: { 65 67 let threshold = 10 66 - for x in [] { x } else { fallback: threshold } 68 + for x in [] { x } fallback { fallbackField: threshold } 67 69 } 68 70 69 71 // if-else: nested body fields - else yields once ··· 86 88 if false { a: b: c: 1 } else { a: b: c: 2 } 87 89 } 88 90 89 - // for-else: nested body fields, empty source 91 + // for-fallback: nested body fields, empty source 90 92 forNestedEmpty: { 91 - for x in [] { a: b: (x): x } else { a: b: none: true } 93 + for x in [] { a: b: (x): x } fallback { a: b: none: true } 92 94 } 93 95 94 - // for-else: nested body fields, non-empty source - no else 96 + // for-fallback: nested body fields, non-empty source - no fallback 95 97 forNestedNonEmpty: { 96 - for x in [1] { a: b: "\(x)": x } else { a: b: none: true } 98 + for x in [1] { a: b: "\(x)": x } fallback { a: b: none: true } 97 99 } 98 100 99 - // nested comprehensions - outer else triggers 100 - nestedOuterElse: { 101 - for x in [] { for y in [1] { y } else { inner: true } } else { outer: true } 101 + // nested comprehensions - outer fallback triggers 102 + nestedOuterFallback: { 103 + for x in [] { for y in [1] { y } fallback { inner: true } } fallback { outer: true } 102 104 } 103 105 104 - // nested comprehensions - inner else triggers 105 - nestedInnerElse: { 106 - for x in [1] { for y in [] { y } else { inner: true } } else { outer: true } 106 + // nested comprehensions - inner fallback triggers 107 + nestedInnerFallback: { 108 + for x in [1] { for y in [] { y } fallback { inner: true } } fallback { outer: true } 107 109 } 108 110 -- out/eval/stats -- 109 111 Leaks: 0 ··· 162 164 } 163 165 outerScope: (struct){ 164 166 let threshold#1 = (int){ 10 } 165 - fallback: (int){ 10 } 167 + fallbackField: (int){ 10 } 166 168 } 167 169 ifNestedBody: (struct){ 168 170 c: (int){ 2 } ··· 198 200 } 199 201 } 200 202 } 201 - nestedOuterElse: (struct){ 203 + nestedOuterFallback: (struct){ 202 204 outer: (bool){ true } 203 205 } 204 - nestedInnerElse: (struct){ 206 + nestedInnerFallback: (struct){ 205 207 inner: (bool){ true } 206 208 } 207 209 } ··· 313 315 for _, x in [] { 314 316 〈1;x〉 315 317 } else { 316 - fallback: 〈1;let threshold#1〉 318 + fallbackField: 〈1;let threshold#1〉 317 319 } 318 320 } 319 321 ifNestedBody: { ··· 392 394 } 393 395 } 394 396 } 395 - nestedOuterElse: { 397 + nestedOuterFallback: { 396 398 for _, x in [] { 397 399 for _, y in [ 398 400 1, ··· 405 407 outer: true 406 408 } 407 409 } 408 - nestedInnerElse: { 410 + nestedInnerFallback: { 409 411 for _, x in [ 410 412 1, 411 413 ] {
+8 -6
cue/testdata/comprehensions/else_compile_errors.txtar
··· 1 1 -- in.cue -- 2 + @experiment(try) 3 + 2 4 // else cannot access for variable 3 5 forVarInElse: { 4 - for i in [1,2,3] { "\(i)": i } else { bad: i } 6 + for i in [1,2,3] { "\(i)": i } fallback { bad: i } 5 7 } 6 8 7 9 // else cannot access let variable from comprehension 8 10 letVarInElse: { 9 - for x in [1,2,3] let y = x*2 { "\(x)": y } else { bad: y } 11 + for x in [1,2,3] let y = x*2 { "\(x)": y } fallback { bad: y } 10 12 } 11 13 -- out/eval/stats -- 12 14 Leaks: 0 ··· 22 24 NumCloseIDs: 0 23 25 -- out/evalalpha -- 24 26 forVarInElse.bad: reference "i" not found: 25 - ./in.cue:3:45 27 + ./in.cue:5:49 26 28 letVarInElse.bad: reference "y" not found: 27 - ./in.cue:8:57 29 + ./in.cue:10:61 28 30 -- out/compile -- 29 31 forVarInElse.bad: reference "i" not found: 30 - ./in.cue:3:45 32 + ./in.cue:5:49 31 33 letVarInElse.bad: reference "y" not found: 32 - ./in.cue:8:57 34 + ./in.cue:10:61 33 35 --- in.cue 34 36 { 35 37 forVarInElse: {
+14 -12
cue/testdata/comprehensions/else_on_error.txtar
··· 1 1 -- in.cue -- 2 + @experiment(try) 3 + 2 4 // When the source expression has an error, the error propagates 3 5 // rather than triggering the else clause. 4 6 5 7 // Error in for source - else should NOT trigger 6 8 forSourceError: { 7 - for x in "not-a-list" { "\(x)": x } else { fallback: true } 9 + for x in "not-a-list" { "\(x)": x } fallback { fallbackField: true } 8 10 } 9 11 10 12 // Error in if condition - else should NOT trigger ··· 14 16 15 17 // Bottom value in for source - else should NOT trigger 16 18 forSourceBottom: { 17 - for x in _|_ { "\(x)": x } else { fallback: true } 19 + for x in _|_ { "\(x)": x } fallback { fallbackField: true } 18 20 } 19 21 -- out/evalalpha -- 20 22 Errors: 21 23 forSourceError: cannot range over "not-a-list" (found string, want list or struct): 22 - ./in.cue:6:11 24 + ./in.cue:8:11 23 25 ifConditionError: invalid operands "not" and 1 to '+' (type string and int): 24 - ./in.cue:11:6 25 - ./in.cue:11:14 26 + ./in.cue:13:6 27 + ./in.cue:13:14 26 28 explicit error (_|_ literal) in source: 27 - ./in.cue:16:11 29 + ./in.cue:18:11 28 30 29 31 Result: 30 32 (_|_){ 31 33 // [eval] 32 34 forSourceError: (_|_){ 33 35 // [eval] forSourceError: cannot range over "not-a-list" (found string, want list or struct): 34 - // ./in.cue:6:11 36 + // ./in.cue:8:11 35 37 } 36 38 ifConditionError: (_|_){ 37 39 // [eval] ifConditionError: invalid operands "not" and 1 to '+' (type string and int): 38 - // ./in.cue:11:6 39 - // ./in.cue:11:14 40 + // ./in.cue:13:6 41 + // ./in.cue:13:14 40 42 } 41 43 forSourceBottom: (_|_){ 42 44 // [user] explicit error (_|_ literal) in source: 43 - // ./in.cue:16:11 45 + // ./in.cue:18:11 44 46 } 45 47 } 46 48 -- out/compile -- ··· 50 52 for _, x in "not-a-list" { 51 53 "\(〈1;x〉)": 〈1;x〉 52 54 } else { 53 - fallback: true 55 + fallbackField: true 54 56 } 55 57 } 56 58 ifConditionError: { ··· 64 66 for _, x in _|_(explicit error (_|_ literal) in source) { 65 67 "\(〈1;x〉)": 〈1;x〉 66 68 } else { 67 - fallback: true 69 + fallbackField: true 68 70 } 69 71 } 70 72 }
+7 -6
cue/token/token.go
··· 100 100 101 101 keywordBeg 102 102 103 - IF // if 104 - ELSE // else 105 - FOR // for 106 - IN // in 107 - LET // let 108 - TRY // try 103 + IF // if 104 + ELSE // else 105 + FOR // for 106 + IN // in 107 + LET // let 108 + TRY // try 109 + FALLBACK // fallback 109 110 // experimental 110 111 FUNC // func 111 112
+8 -7
cue/token/token_string.go
··· 66 66 _ = x[IN-55] 67 67 _ = x[LET-56] 68 68 _ = x[TRY-57] 69 - _ = x[FUNC-58] 70 - _ = x[TRUE-59] 71 - _ = x[FALSE-60] 72 - _ = x[NULL-61] 73 - _ = x[keywordEnd-62] 69 + _ = x[FALLBACK-58] 70 + _ = x[FUNC-59] 71 + _ = x[TRUE-60] 72 + _ = x[FALSE-61] 73 + _ = x[NULL-62] 74 + _ = x[keywordEnd-63] 74 75 } 75 76 76 - const _Token_name = "ILLEGALEOFCOMMENTATTRIBUTEliteralBegIDENTINTFLOATSTRINGINTERPOLATION_|_literalEndoperatorBeg+-*^/quoremdivmod&|&&||===<>!<-!=<=>==~!~([{,....)]};:?~operatorEndkeywordBegifelseforinlettryfunctruefalsenullkeywordEnd" 77 + const _Token_name = "ILLEGALEOFCOMMENTATTRIBUTEliteralBegIDENTINTFLOATSTRINGINTERPOLATION_|_literalEndoperatorBeg+-*^/quoremdivmod&|&&||===<>!<-!=<=>==~!~([{,....)]};:?~operatorEndkeywordBegifelseforinlettryfallbackfunctruefalsenullkeywordEnd" 77 78 78 - var _Token_index = [...]uint8{0, 7, 10, 17, 26, 36, 41, 44, 49, 55, 68, 71, 81, 92, 93, 94, 95, 96, 97, 100, 103, 106, 109, 110, 111, 113, 115, 116, 118, 119, 120, 121, 123, 125, 127, 129, 131, 133, 134, 135, 136, 137, 138, 141, 142, 143, 144, 145, 146, 147, 148, 159, 169, 171, 175, 178, 180, 183, 186, 190, 194, 199, 203, 213} 79 + var _Token_index = [...]uint8{0, 7, 10, 17, 26, 36, 41, 44, 49, 55, 68, 71, 81, 92, 93, 94, 95, 96, 97, 100, 103, 106, 109, 110, 111, 113, 115, 116, 118, 119, 120, 121, 123, 125, 127, 129, 131, 133, 134, 135, 136, 137, 138, 141, 142, 143, 144, 145, 146, 147, 148, 159, 169, 171, 175, 178, 180, 183, 186, 194, 198, 202, 207, 211, 221} 79 80 80 81 func (i Token) String() string { 81 82 idx := int(i) - 0
+2 -35
doc/ref/spec.md
··· 254 254 The following keywords are used in comprehensions. 255 255 256 256 ``` 257 - for in if let else 257 + for in if let 258 258 ``` 259 259 260 260 <!-- ··· 2626 2626 struct. 2627 2627 Both structs and lists may contain multiple comprehensions. 2628 2628 2629 - A comprehension may have an optional `else` clause. 2630 - The else clause is evaluated and its contents yielded 2631 - if the comprehension completes without yielding any values. 2632 - For an `if` comprehension, this occurs when the condition is false. 2633 - For a `for` comprehension, this occurs when the source is empty 2634 - or when all iterations are filtered out by subsequent `if` clauses. 2635 - 2636 - The else clause body is evaluated in the enclosing scope, 2637 - not the comprehension's internal scope. 2638 - This means identifiers bound by `for` or `let` clauses 2639 - are not accessible within the else clause. 2640 - 2641 - If the comprehension source or condition contains an error, 2642 - the error propagates rather than triggering the else clause. 2643 - 2644 2629 ``` 2645 - Comprehension = Clauses StructLit [ ElseClause ] . 2630 + Comprehension = Clauses StructLit . 2646 2631 2647 2632 Clauses = StartClause { [ "," ] Clause } . 2648 2633 StartClause = ForClause | GuardClause . ··· 2665 2650 } 2666 2651 } 2667 2652 d: { "1": 2, "2": 3, "3": 4 } 2668 - 2669 - // else clause examples 2670 - e: { 2671 - if false { a: 1 } else { b: 2 } 2672 - } 2673 - f: { b: 2 } 2674 - 2675 - g: { 2676 - for x in [] { "\(x)": x } else { empty: true } 2677 - } 2678 - h: { empty: true } 2679 - 2680 - // else clause accesses outer scope 2681 - i: { 2682 - let threshold = 10 2683 - for x in [] { x } else { fallback: threshold } 2684 - } 2685 - j: { fallback: 10 } 2686 2653 ``` 2687 2654 2688 2655
+116 -60
doc/specs/comprehension-else/spec.md
··· 1 - # Comprehension Else Clause 1 + # Comprehension Fallback Clause 2 2 3 3 ## Purpose 4 4 5 - Provides an optional `else` clause for comprehensions that yields a fallback value when the comprehension produces zero values. This enables concise handling of empty collections and failed filter conditions without external conditionals. 5 + Provides an optional fallback clause for comprehensions that yields a fallback value when the comprehension produces zero values. This enables concise handling of empty collections and failed filter conditions without external conditionals. 6 + 7 + ## Keywords 8 + 9 + The keyword used depends on the comprehension type: 10 + - `for` comprehensions use the `fallback` keyword 11 + - `if` comprehensions use the `else` keyword 12 + - `try` comprehensions use the `else` keyword 13 + 14 + This distinction provides semantic clarity: `else` implies a binary choice (true/false, success/failure), while `fallback` indicates a default when no results are produced. 6 15 7 16 ## Requirements 8 17 9 - ### Requirement: Else clause syntax 18 + ### Requirement: Fallback clause syntax 10 19 11 - The parser SHALL accept an `else` keyword followed by a StructLit as an optional terminal clause in a comprehension. The grammar SHALL be: 20 + The parser SHALL accept an `else` keyword followed by a StructLit as an optional terminal clause after `if` or `try` clauses, and a `fallback` keyword followed by a StructLit as an optional terminal clause after `for` clauses. The grammar SHALL be: 12 21 13 22 ``` 14 - Comprehension = Clauses StructLit [ ElseClause ] . 23 + Comprehension = Clauses StructLit [ ElseClause | FallbackClause ] . 15 24 ElseClause = "else" StructLit . 25 + FallbackClause = "fallback" StructLit . 16 26 ``` 17 27 18 28 #### Scenario: Parse if comprehension with else 19 29 - **WHEN** the input is `if enabled { a: 1 } else { b: 2 }` 20 30 - **THEN** the parser SHALL produce an AST with an IfClause, a StructLit body, and an ElseClause with its own StructLit 21 31 22 - #### Scenario: Parse for comprehension with else 23 - - **WHEN** the input is `for x in list { (x): true } else { empty: true }` 24 - - **THEN** the parser SHALL produce an AST with a ForClause, a StructLit body, and an ElseClause with its own StructLit 32 + #### Scenario: Parse for comprehension with fallback 33 + - **WHEN** the input is `for x in list { (x): true } fallback { empty: true }` 34 + - **THEN** the parser SHALL produce an AST with a ForClause, a StructLit body, and a FallbackClause with its own StructLit 25 35 26 - #### Scenario: Parse multi-clause comprehension with else 27 - - **WHEN** the input is `for x in list if x > 0 let y = x * 2 { (y): x } else { none: true }` 28 - - **THEN** the parser SHALL produce an AST with ForClause, IfClause, LetClause, a StructLit body, and an ElseClause 36 + #### Scenario: Parse multi-clause comprehension with fallback 37 + - **WHEN** the input is `for x in list if x > 0 let y = x * 2 { (y): x } fallback { none: true }` 38 + - **THEN** the parser SHALL produce an AST with ForClause, IfClause, LetClause, a StructLit body, and a FallbackClause 29 39 30 - #### Scenario: Comprehension without else remains valid 40 + #### Scenario: Comprehension without else or fallback remains valid 31 41 - **WHEN** the input is `if enabled { a: 1 }` 32 - - **THEN** the parser SHALL produce an AST with an IfClause and a StructLit body, with no ElseClause 42 + - **THEN** the parser SHALL produce an AST with an IfClause and a StructLit body, with no ElseClause or FallbackClause 33 43 34 - ### Requirement: Single else clause limit 44 + ### Requirement: Keyword-clause validation 35 45 36 - A comprehension SHALL have at most one else clause. If multiple else clauses are present, the parser SHALL report an error. 46 + The parser SHALL validate that the correct keyword is used based on the preceding clause type: 47 + - `else` is valid only after `if` or `try` clauses 48 + - `fallback` is valid only after `for` clauses 49 + 50 + #### Scenario: else after if accepted 51 + - **WHEN** the input is `if enabled { a: 1 } else { b: 2 }` 52 + - **THEN** the parser SHALL accept this as valid 53 + 54 + #### Scenario: fallback after if rejected 55 + - **WHEN** the input is `if enabled { a: 1 } fallback { b: 2 }` 56 + - **THEN** the parser SHALL report an error: "use 'else' with 'if' clauses" 57 + 58 + #### Scenario: fallback after for accepted 59 + - **WHEN** the input is `for x in list { (x): true } fallback { empty: true }` 60 + - **THEN** the parser SHALL accept this as valid 61 + 62 + #### Scenario: else after for rejected 63 + - **WHEN** the input is `for x in list { (x): true } else { empty: true }` 64 + - **THEN** the parser SHALL report an error: "use 'fallback' with 'for' clauses" 65 + 66 + #### Scenario: fallback after try rejected 67 + - **WHEN** the input is `try { a: x? } fallback { b: 2 }` 68 + - **THEN** the parser SHALL report an error: "use 'else' with 'try' clauses" 69 + 70 + ### Requirement: Single else or fallback clause limit 71 + 72 + A comprehension SHALL have at most one else or fallback clause. If multiple such clauses are present, the parser SHALL report an error. 37 73 38 74 #### Scenario: Multiple else clauses rejected 39 75 - **WHEN** the input is `if enabled { a: 1 } else { b: 2 } else { c: 3 }` 40 76 - **THEN** the parser SHALL report an error indicating multiple else clauses are not allowed 41 77 78 + #### Scenario: Multiple fallback clauses rejected 79 + - **WHEN** the input is `for x in list { x } fallback { a: 1 } fallback { b: 2 }` 80 + - **THEN** the parser SHALL report an error indicating multiple fallback clauses are not allowed 81 + 42 82 ### Requirement: Else token 43 83 44 84 The lexer SHALL recognize `else` as a keyword token. ··· 47 87 - **WHEN** the input contains the identifier `else` 48 88 - **THEN** the lexer SHALL emit an ELSE token, not an IDENT token 49 89 90 + ### Requirement: Fallback token 91 + 92 + The lexer SHALL recognize `fallback` as a keyword token. 93 + 94 + #### Scenario: Fallback as keyword 95 + - **WHEN** the input contains the identifier `fallback` 96 + - **THEN** the lexer SHALL emit a FALLBACK token, not an IDENT token 97 + 98 + ### Requirement: Keywords as field labels 99 + 100 + The parser SHALL accept `else` and `fallback` keywords as valid field labels. 101 + 102 + #### Scenario: Else and fallback as field labels 103 + - **WHEN** the input contains `else: 1` or `fallback: 2` as field definitions 104 + - **THEN** the parser SHALL accept these as valid field labels 105 + 50 106 ### Requirement: If-else semantics 51 107 52 108 When a comprehension has an if clause and an else clause, the else clause's StructLit SHALL be yielded exactly once if and only if the comprehension yields zero values. ··· 63 119 - **WHEN** evaluating `{ if x { a: 1 } else { b: 2 } }` where `x` is not a boolean 64 120 - **THEN** the evaluation SHALL report a type error (existing behavior, unaffected by else) 65 121 66 - ### Requirement: For-else semantics 122 + ### Requirement: For-fallback semantics 67 123 68 - When a comprehension starts with a for clause and has an else clause, the else clause's StructLit SHALL be yielded exactly once if and only if the for loop (including any subsequent filtering if clauses) yields zero values. 124 + When a comprehension starts with a for clause and has a fallback clause, the fallback clause's StructLit SHALL be yielded exactly once if and only if the for loop (including any subsequent filtering if clauses) yields zero values. 69 125 70 - #### Scenario: For with non-empty source - no else 71 - - **WHEN** evaluating `{ for x in [1, 2] { "\(x)": x } else { empty: true } }` 126 + #### Scenario: For with non-empty source - no fallback 127 + - **WHEN** evaluating `{ for x in [1, 2] { "\(x)": x } fallback { empty: true } }` 72 128 - **THEN** the result SHALL be `{ "1": 1, "2": 2 }` 73 129 74 - #### Scenario: For with empty source - else yields 75 - - **WHEN** evaluating `{ for x in [] { "\(x)": x } else { empty: true } }` 130 + #### Scenario: For with empty source - fallback yields 131 + - **WHEN** evaluating `{ for x in [] { "\(x)": x } fallback { empty: true } }` 76 132 - **THEN** the result SHALL be `{ empty: true }` 77 133 78 - #### Scenario: For with filter removing all - else yields 79 - - **WHEN** evaluating `{ for x in [1, 2, 3] if x > 10 { "\(x)": x } else { empty: true } }` 134 + #### Scenario: For with filter removing all - fallback yields 135 + - **WHEN** evaluating `{ for x in [1, 2, 3] if x > 10 { "\(x)": x } fallback { empty: true } }` 80 136 - **THEN** the result SHALL be `{ empty: true }` 81 137 82 - #### Scenario: For with filter keeping some - no else 83 - - **WHEN** evaluating `{ for x in [1, 2, 3] if x > 1 { "\(x)": x } else { empty: true } }` 138 + #### Scenario: For with filter keeping some - no fallback 139 + - **WHEN** evaluating `{ for x in [1, 2, 3] if x > 1 { "\(x)": x } fallback { empty: true } }` 84 140 - **THEN** the result SHALL be `{ "2": 2, "3": 3 }` 85 141 86 - ### Requirement: List comprehension else 142 + ### Requirement: List comprehension else and fallback 87 143 88 - The else clause SHALL work in list comprehensions, yielding the else struct's contents as list elements. 144 + The else clause SHALL work in list comprehensions with `if`, and the fallback clause SHALL work in list comprehensions with `for`, yielding the clause's contents as list elements. 89 145 90 - #### Scenario: List for-else with empty source 91 - - **WHEN** evaluating `[ for x in [] { x } else { 0 } ]` 146 + #### Scenario: List for-fallback with empty source 147 + - **WHEN** evaluating `[ for x in [] { x } fallback { 0 } ]` 92 148 - **THEN** the result SHALL be `[0]` 93 149 94 - #### Scenario: List for-else with non-empty source 95 - - **WHEN** evaluating `[ for x in [1, 2] { x * 2 } else { 0 } ]` 150 + #### Scenario: List for-fallback with non-empty source 151 + - **WHEN** evaluating `[ for x in [1, 2] { x * 2 } fallback { 0 } ]` 96 152 - **THEN** the result SHALL be `[2, 4]` 97 153 98 154 #### Scenario: List if-else true ··· 103 159 - **WHEN** evaluating `[ if false { 1 } else { 2 } ]` 104 160 - **THEN** the result SHALL be `[2]` 105 161 106 - ### Requirement: Else clause scoping 162 + ### Requirement: Fallback clause scoping 107 163 108 - The else clause SHALL have access to the enclosing scope but SHALL NOT have access to variables bound by for or let clauses within the comprehension. 164 + The fallback clause SHALL have access to the enclosing scope but SHALL NOT have access to variables bound by for or let clauses within the comprehension. 109 165 110 - **Rationale**: When `else` triggers, the comprehension yielded zero values, meaning `for` variables have no meaningful value. While `let` variables that don't depend on `for` variables could theoretically be accessible, allowing partial access creates subtle bugs: code that works when at least one iteration succeeds would fail mysteriously when all iterations are filtered out. A simple, uniform rule (no comprehension-internal variables in `else`) is predictable and avoids these edge cases. Users needing shared values can bind them in the outer scope before the comprehension. 166 + **Rationale**: When the fallback triggers, the comprehension yielded zero values, meaning `for` variables have no meaningful value. While `let` variables that don't depend on `for` variables could theoretically be accessible, allowing partial access creates subtle bugs: code that works when at least one iteration succeeds would fail mysteriously when all iterations are filtered out. A simple, uniform rule (no comprehension-internal variables in the fallback clause) is predictable and avoids these edge cases. Users needing shared values can bind them in the outer scope before the comprehension. 111 167 112 - #### Scenario: Else accesses outer scope 113 - - **WHEN** evaluating `{ outer: 1, result: { for x in [] { x } else { fallback: outer } }.result }` 114 - - **THEN** the result SHALL include `result: { fallback: 1 }` 168 + #### Scenario: Fallback accesses outer scope 169 + - **WHEN** evaluating `{ outer: 1, result: { for x in [] { x } fallback { fallbackField: outer } }.result }` 170 + - **THEN** the result SHALL include `result: { fallbackField: 1 }` 115 171 116 - #### Scenario: Else cannot access for variables 117 - - **WHEN** evaluating `{ for x in [] { x } else { bad: x } }` 172 + #### Scenario: Fallback cannot access for variables 173 + - **WHEN** evaluating `{ for x in [] { x } fallback { bad: x } }` 118 174 - **THEN** the evaluation SHALL report an error that `x` is undefined 119 175 120 - #### Scenario: Else cannot access let variables 121 - - **WHEN** evaluating `{ for x in [] let y = 1 { y } else { bad: y } }` 176 + #### Scenario: Fallback cannot access let variables 177 + - **WHEN** evaluating `{ for x in [] let y = 1 { y } fallback { bad: y } }` 122 178 - **THEN** the evaluation SHALL report an error that `y` is undefined 123 179 124 - ### Requirement: Else with nested comprehensions 180 + ### Requirement: Else and fallback with nested comprehensions 125 181 126 - When comprehensions are nested, each else clause SHALL apply only to its immediately enclosing comprehension. 182 + When comprehensions are nested, each else or fallback clause SHALL apply only to its immediately enclosing comprehension. 127 183 128 - #### Scenario: Outer else triggers, inner does not 129 - - **WHEN** evaluating `{ for x in [] { for y in [1] { y } else { inner: true } } else { outer: true } }` 130 - - **THEN** the result SHALL be `{ outer: true }` (outer else triggers because outer for is empty) 184 + #### Scenario: Outer fallback triggers, inner does not 185 + - **WHEN** evaluating `{ for x in [] { for y in [1] { y } fallback { inner: true } } fallback { outer: true } }` 186 + - **THEN** the result SHALL be `{ outer: true }` (outer fallback triggers because outer for is empty) 131 187 132 - #### Scenario: Inner else triggers, outer does not 133 - - **WHEN** evaluating `{ for x in [1] { for y in [] { y } else { inner: true } } else { outer: true } }` 134 - - **THEN** the result SHALL be `{ inner: true }` (inner else triggers, outer for yielded) 188 + #### Scenario: Inner fallback triggers, outer does not 189 + - **WHEN** evaluating `{ for x in [1] { for y in [] { y } fallback { inner: true } } fallback { outer: true } }` 190 + - **THEN** the result SHALL be `{ inner: true }` (inner fallback triggers, outer for yielded) 135 191 136 - ### Requirement: Else does not trigger on errors 192 + ### Requirement: Fallback does not trigger on errors 137 193 138 - When a comprehension's body produces errors on all iterations, the else clause SHALL NOT be triggered. Errors SHALL propagate normally. 194 + When a comprehension's body produces errors on all iterations, the fallback clause SHALL NOT be triggered. Errors SHALL propagate normally. 139 195 140 - #### Scenario: Error in body does not trigger else 141 - - **WHEN** evaluating `{ for x in [1] { bad: x.nonexistent } else { fallback: true } }` 142 - - **THEN** the evaluation SHALL report an error, not yield `{ fallback: true }` 196 + #### Scenario: Error in body does not trigger fallback 197 + - **WHEN** evaluating `{ for x in [1] { bad: x.nonexistent } fallback { fallbackField: true } }` 198 + - **THEN** the evaluation SHALL report an error, not yield `{ fallbackField: true }` 143 199 144 - ### Requirement: Else with struct embedding 200 + ### Requirement: Fallback with struct embedding 145 201 146 - The else clause's StructLit SHALL be embedded in the enclosing struct, following the same embedding rules as the main comprehension body. 202 + The fallback clause's StructLit SHALL be embedded in the enclosing struct, following the same embedding rules as the main comprehension body. 147 203 148 - #### Scenario: Else embeds fields 149 - - **WHEN** evaluating `{ existing: 1, if false { added: 2 } else { fallback: 3 } }` 150 - - **THEN** the result SHALL be `{ existing: 1, fallback: 3 }` 204 + #### Scenario: Fallback embeds fields 205 + - **WHEN** evaluating `{ existing: 1, if false { added: 2 } else { fallbackField: 3 } }` 206 + - **THEN** the result SHALL be `{ existing: 1, fallbackField: 3 }` 151 207 152 - #### Scenario: Else can contain multiple fields 208 + #### Scenario: Fallback can contain multiple fields 153 209 - **WHEN** evaluating `{ if false { a: 1 } else { b: 2, c: 3 } }` 154 210 - **THEN** the result SHALL be `{ b: 2, c: 3 }`
+2
encoding/jsonschema/crd.cue
··· 1 + @experiment(try) 2 + 1 3 package jsonschema 2 4 3 5 // input holds the parsed YAML document, which may contain multiple
+9 -2
internal/astinternal/debug.go
··· 421 421 case *ast.Comprehension: 422 422 out := DebugStr(v.Clauses) 423 423 out += DebugStr(v.Value) 424 - if v.Else != nil { 425 - out += " else " + DebugStr(v.Else.Body) 424 + if v.Fallback != nil { 425 + // Use "fallback" for 'for' comprehensions, "else" for 'if'/'try' 426 + kw := "else" 427 + if len(v.Clauses) > 0 { 428 + if _, ok := v.Clauses[0].(*ast.ForClause); ok { 429 + kw = "fallback" 430 + } 431 + } 432 + out += " " + kw + " " + DebugStr(v.Fallback.Body) 426 433 } 427 434 return out 428 435
+1
internal/ci/base/github.cue
··· 1 1 @experiment(explicitopen) 2 + @experiment(try) 2 3 3 4 package base 4 5
+2
internal/ci/base/helpers.cue
··· 1 + @experiment(try) 2 + 1 3 package base 2 4 3 5 // This file contains everything else
+3 -3
internal/core/adt/comprehension.go
··· 259 259 } 260 260 // If there's an else clause, we still need to schedule a task 261 261 // to handle the fallback case when comprehension yields zero values. 262 - if c.Else == nil { 262 + if c.Fallback == nil { 263 263 return 264 264 } 265 265 // Use an empty struct as the main value since all fields were ··· 381 381 382 382 if len(d.envs) == 0 { 383 383 // If there's an else clause, use it instead of marking arc as not present. 384 - if d.leaf.Else != nil { 384 + if d.leaf.Fallback != nil { 385 385 // Evaluate the else clause in the outer environment. 386 386 // We use linkChildren to properly chain the environment, similar to 387 387 // normal comprehension yield processing. 388 388 env := linkChildren(d.env, d.leaf) 389 - n.scheduleConjunct(Conjunct{env, d.leaf.Else, d.id}, d.id) 389 + n.scheduleConjunct(Conjunct{env, d.leaf.Fallback, d.id}, d.id) 390 390 return nil 391 391 } 392 392 n.node.updateArcType(ArcNotPresent)
+2 -2
internal/core/adt/expr.go
··· 1927 1927 // information as possible. 1928 1928 Value Node 1929 1929 1930 - // Else is the optional else clause that is yielded when the comprehension 1930 + // Fallback is the optional else clause that is yielded when the comprehension 1931 1931 // produces zero values. 1932 - Else *StructLit 1932 + Fallback *StructLit 1933 1933 1934 1934 // The type of field as which the comprehension is added. 1935 1935 arcType ArcType
+2 -2
internal/core/adt/tasks.go
··· 288 288 } 289 289 // If comprehension yielded zero values and has an else clause, 290 290 // insert the else clause's struct contents as list elements. 291 - if index == indexBefore && x.Else != nil { 291 + if index == indexBefore && x.Fallback != nil { 292 292 label, err := MakeLabel(x.Source(), index, IntLabel) 293 293 n.addErr(err) 294 294 index++ 295 - conj := MakeConjunct(t.env, x.Else, id) 295 + conj := MakeConjunct(t.env, x.Fallback, id) 296 296 n.insertArc(label, ArcMember, conj, id, true) 297 297 } 298 298
+16 -10
internal/core/compile/compile.go
··· 1039 1039 Value: st, 1040 1040 } 1041 1041 1042 - // Compile else clause in the outer scope (before comprehension variables). 1043 - // The else clause should NOT have access to for/let variables. 1044 - if x.Else != nil { 1045 - if err := c.requireVersion(x.Else, "v0.16.0", "else clause in comprehension"); err != nil { 1046 - return err 1042 + // Compile fallback clause in the outer scope (before comprehension variables). 1043 + // The fallback clause should NOT have access to for/let variables. 1044 + if x.Fallback != nil { 1045 + // Both 'else' (with if) and 'fallback' (with for) require the try experiment. 1046 + if !c.experiments.Try { 1047 + // Use appropriate keyword in error message based on clause type. 1048 + keyword := "else" 1049 + if _, isFor := x.Clauses[0].(*ast.ForClause); isFor { 1050 + keyword = "fallback" 1051 + } 1052 + return c.errf(x.Fallback, "%s clause requires the try experiment (language version v0.16.0 or later)", keyword) 1047 1053 } 1048 - // Pop all comprehension scopes temporarily to compile else in outer scope. 1049 - // We need to compile the else body outside the comprehension's scope chain. 1054 + // Pop all comprehension scopes temporarily to compile fallback in outer scope. 1055 + // We need to compile the fallback body outside the comprehension's scope chain. 1050 1056 savedStack := c.stack 1051 1057 // Find the scope depth before the comprehension clauses were pushed. 1052 1058 // Each for/let clause pushes one scope. ··· 1059 1065 } 1060 1066 // Temporarily truncate to outer scope depth. 1061 1067 c.stack = savedStack[:outerDepth] 1062 - elseBody := c.expr(x.Else.Body) 1068 + fallbackBody := c.expr(x.Fallback.Body) 1063 1069 c.stack = savedStack // Restore full stack 1064 - if elseSt, ok := elseBody.(*adt.StructLit); ok { 1065 - comp.Else = elseSt 1070 + if fallbackSt, ok := fallbackBody.(*adt.StructLit); ok { 1071 + comp.Fallback = fallbackSt 1066 1072 } 1067 1073 } 1068 1074
+2 -2
internal/core/debug/compact.go
··· 361 361 w.node(c) 362 362 } 363 363 w.node(adt.ToExpr(x.Value)) 364 - if x.Else != nil { 364 + if x.Fallback != nil { 365 365 w.string(" else ") 366 - w.node(x.Else) 366 + w.node(x.Fallback) 367 367 } 368 368 369 369 case *adt.ForClause:
+2 -2
internal/core/debug/debug.go
··· 715 715 w.node(c) 716 716 } 717 717 w.node(adt.ToExpr(x.Value)) 718 - if x.Else != nil { 718 + if x.Fallback != nil { 719 719 w.string(" else ") 720 - w.node(x.Else) 720 + w.node(x.Fallback) 721 721 } 722 722 723 723 case *adt.ForClause:
+2 -2
internal/core/dep/dep.go
··· 695 695 c.markExpr(env, adt.ToExpr(y.Value)) 696 696 697 697 // Mark else clause in outer environment. 698 - if y.Else != nil { 699 - c.markExpr(outerEnv, y.Else) 698 + if y.Fallback != nil { 699 + c.markExpr(outerEnv, y.Fallback) 700 700 } 701 701 } 702 702
+2 -2
internal/core/dep/mixed.go
··· 134 134 135 135 func (m marked) markComprehension(y *adt.Comprehension) { 136 136 m.markExpr(adt.ToExpr(y.Value)) 137 - if y.Else != nil { 138 - m.markExpr(y.Else) 137 + if y.Fallback != nil { 138 + m.markExpr(y.Fallback) 139 139 } 140 140 }
+44 -3
internal/core/dep/testdata/else.txtar
··· 1 1 -- in.cue -- 2 - // Test that dependencies in else clause are tracked 2 + @experiment(try) 3 3 4 - // else clause references outer field - else triggers because list is empty 4 + // Test that dependencies in fallback clause are tracked 5 + 6 + // fallback clause references outer field - fallback triggers because list is empty 5 7 a: b: { 6 - for x in c { "\(x)": x } else { fallback: d } 8 + for x in c { "\(x)": x } fallback { fallbackField: d } 7 9 } 8 10 c: [] 9 11 d: 10 12 + -- out/dependencies-v3/field -- 13 + line reference path of resulting vertex 14 + 7: c => c 15 + -- diff/-out/dependencies-v3/field<==>+out/dependencies/field -- 16 + diff old new 17 + --- old 18 + +++ new 19 + @@ -1,2 +1,2 @@ 20 + line reference path of resulting vertex 21 + -5: c => c 22 + +7: c => c 10 23 -- out/dependencies/field -- 11 24 line reference path of resulting vertex 12 25 5: c => c 26 + -- out/dependencies-v3/all -- 27 + line reference path of resulting vertex 28 + 7: c => c 29 + 7: d => d 30 + -- diff/-out/dependencies-v3/all<==>+out/dependencies/all -- 31 + diff old new 32 + --- old 33 + +++ new 34 + @@ -1,3 +1,3 @@ 35 + line reference path of resulting vertex 36 + -5: c => c 37 + -5: d => d 38 + +7: c => c 39 + +7: d => d 13 40 -- out/dependencies/all -- 14 41 line reference path of resulting vertex 15 42 5: c => c 16 43 5: d => d 44 + -- out/dependencies-v3/dynamic -- 45 + line reference path of resulting vertex 46 + 7: c => c 47 + 7: d => d 48 + -- diff/-out/dependencies-v3/dynamic<==>+out/dependencies/dynamic -- 49 + diff old new 50 + --- old 51 + +++ new 52 + @@ -1,3 +1,3 @@ 53 + line reference path of resulting vertex 54 + -5: c => c 55 + -5: d => d 56 + +7: c => c 57 + +7: d => d 17 58 -- out/dependencies/dynamic -- 18 59 line reference path of resulting vertex 19 60 5: c => c
+5 -5
internal/core/export/adt.go
··· 831 831 } 832 832 c.Value = v 833 833 834 - // Export else clause using outer environment. 835 - if comp.Else != nil { 836 - elseBody := e.expr(outerEnv, comp.Else) 837 - if body, ok := elseBody.(*ast.StructLit); ok { 838 - c.Else = &ast.ElseClause{Body: body} 834 + // Export fallback clause using outer environment. 835 + if comp.Fallback != nil { 836 + fallbackBody := e.expr(outerEnv, comp.Fallback) 837 + if body, ok := fallbackBody.(*ast.StructLit); ok { 838 + c.Fallback = &ast.FallbackClause{Body: body} 839 839 } 840 840 } 841 841
+231 -29
internal/core/export/testdata/main/else.txtar
··· 1 1 -- a.cue -- 2 - // Test export of else clause in comprehensions 2 + @experiment(try) 3 + 4 + // Test export of fallback clause in comprehensions 3 5 4 6 // if-else 5 7 ifElse: { 6 8 if false { a: 1 } else { b: 2 } 7 9 } 8 10 9 - // for-else with empty list 10 - forElse: { 11 - for x in [] { "\(x)": x } else { empty: true } 11 + // for-fallback with empty list 12 + forFallback: { 13 + for x in [] { "\(x)": x } fallback { empty: true } 14 + } 15 + 16 + // for-fallback with outer scope reference 17 + forFallbackOuter: { 18 + let val = 10 19 + for x in [] { x } fallback { fallbackField: val } 20 + } 21 + -- out/definition-v3-noshare -- 22 + @experiment(try) 23 + 24 + // Test export of fallback clause in comprehensions 25 + 26 + // if-else 27 + ifElse: { 28 + if false { 29 + a: 1 30 + } else { 31 + b: 2 32 + } 33 + } 34 + 35 + // for-fallback with empty list 36 + forFallback: { 37 + for x in [] { 38 + "\(x)": x 39 + } fallback { 40 + empty: true 41 + } 12 42 } 13 43 14 - // for-else with outer scope reference 15 - forElseOuter: { 44 + // for-fallback with outer scope reference 45 + forFallbackOuter: { 16 46 let val = 10 17 - for x in [] { x } else { fallback: val } 47 + for x in [] { 48 + x 49 + } fallback { 50 + fallbackField: val 51 + } 18 52 } 53 + -- diff/-out/definition-v3-noshare<==>+out/definition -- 54 + diff old new 55 + --- old 56 + +++ new 57 + @@ -1,4 +1,7 @@ 58 + -// Test export of else clause in comprehensions 59 + +@experiment(try) 60 + + 61 + +// Test export of fallback clause in comprehensions 62 + + 63 + // if-else 64 + ifElse: { 65 + if false { 66 + @@ -8,21 +11,21 @@ 67 + } 68 + } 69 + 70 + -// for-else with empty list 71 + -forElse: { 72 + +// for-fallback with empty list 73 + +forFallback: { 74 + for x in [] { 75 + "\(x)": x 76 + - } else { 77 + + } fallback { 78 + empty: true 79 + } 80 + } 81 + 82 + -// for-else with outer scope reference 83 + -forElseOuter: { 84 + +// for-fallback with outer scope reference 85 + +forFallbackOuter: { 86 + let val = 10 87 + for x in [] { 88 + x 89 + - } else { 90 + - fallback: val 91 + + } fallback { 92 + + fallbackField: val 93 + } 94 + } 95 + -- out/definition-v3 -- 96 + @experiment(try) 97 + 98 + // Test export of fallback clause in comprehensions 99 + 100 + // if-else 101 + ifElse: { 102 + if false { 103 + a: 1 104 + } else { 105 + b: 2 106 + } 107 + } 108 + 109 + // for-fallback with empty list 110 + forFallback: { 111 + for x in [] { 112 + "\(x)": x 113 + } fallback { 114 + empty: true 115 + } 116 + } 117 + 118 + // for-fallback with outer scope reference 119 + forFallbackOuter: { 120 + let val = 10 121 + for x in [] { 122 + x 123 + } fallback { 124 + fallbackField: val 125 + } 126 + } 127 + -- diff/-out/definition-v3<==>+out/definition -- 128 + diff old new 129 + --- old 130 + +++ new 131 + @@ -1,4 +1,7 @@ 132 + -// Test export of else clause in comprehensions 133 + +@experiment(try) 134 + + 135 + +// Test export of fallback clause in comprehensions 136 + + 137 + // if-else 138 + ifElse: { 139 + if false { 140 + @@ -8,21 +11,21 @@ 141 + } 142 + } 143 + 144 + -// for-else with empty list 145 + -forElse: { 146 + +// for-fallback with empty list 147 + +forFallback: { 148 + for x in [] { 149 + "\(x)": x 150 + - } else { 151 + + } fallback { 152 + empty: true 153 + } 154 + } 155 + 156 + -// for-else with outer scope reference 157 + -forElseOuter: { 158 + +// for-fallback with outer scope reference 159 + +forFallbackOuter: { 160 + let val = 10 161 + for x in [] { 162 + x 163 + - } else { 164 + - fallback: val 165 + + } fallback { 166 + + fallbackField: val 167 + } 168 + } 19 169 -- out/definition -- 20 170 // Test export of else clause in comprehensions 21 171 // if-else ··· 45 195 fallback: val 46 196 } 47 197 } 198 + -- out/doc-v3 -- 199 + [] 200 + [ifElse] 201 + - if-else 202 + 203 + [ifElse b] 204 + [forFallback] 205 + - for-fallback with empty list 206 + 207 + [forFallback empty] 208 + [forFallbackOuter] 209 + - for-fallback with outer scope reference 210 + 211 + [forFallbackOuter val] 212 + [forFallbackOuter fallbackField] 213 + -- diff/-out/doc-v3<==>+out/doc -- 214 + diff old new 215 + --- old 216 + +++ new 217 + @@ -1,16 +1,14 @@ 218 + [] 219 + -- Test export of else clause in comprehensions 220 + - 221 + [ifElse] 222 + - if-else 223 + 224 + [ifElse b] 225 + -[forElse] 226 + -- for-else with empty list 227 + - 228 + -[forElse empty] 229 + -[forElseOuter] 230 + -- for-else with outer scope reference 231 + - 232 + -[forElseOuter val] 233 + -[forElseOuter fallback] 234 + +[forFallback] 235 + +- for-fallback with empty list 236 + + 237 + +[forFallback empty] 238 + +[forFallbackOuter] 239 + +- for-fallback with outer scope reference 240 + + 241 + +[forFallbackOuter val] 242 + +[forFallbackOuter fallbackField] 48 243 -- out/doc -- 49 244 [] 50 245 - Test export of else clause in comprehensions ··· 62 257 63 258 [forElseOuter val] 64 259 [forElseOuter fallback] 65 - -- out/value -- 260 + -- out/value-v3 -- 66 261 == Simplified 67 262 { 68 263 // if-else ··· 70 265 b: 2 71 266 } 72 267 73 - // for-else with empty list 74 - forElse: { 268 + // for-fallback with empty list 269 + forFallback: { 75 270 empty: true 76 271 } 77 272 78 - // for-else with outer scope reference 79 - forElseOuter: { 80 - fallback: 10 273 + // for-fallback with outer scope reference 274 + forFallbackOuter: { 275 + fallbackField: 10 81 276 } 82 277 } 83 278 == Raw ··· 87 282 b: 2 88 283 } 89 284 90 - // for-else with empty list 91 - forElse: { 285 + // for-fallback with empty list 286 + forFallback: { 92 287 empty: true 93 288 } 94 289 95 - // for-else with outer scope reference 96 - forElseOuter: { 97 - fallback: 10 290 + // for-fallback with outer scope reference 291 + forFallbackOuter: { 292 + fallbackField: 10 98 293 } 99 294 } 100 295 == Final ··· 102 297 ifElse: { 103 298 b: 2 104 299 } 105 - forElse: { 300 + forFallback: { 106 301 empty: true 107 302 } 108 - forElseOuter: { 109 - fallback: 10 303 + forFallbackOuter: { 304 + fallbackField: 10 110 305 } 111 306 } 112 307 == All 113 308 { 309 + @experiment(try) 310 + 311 + // Test export of fallback clause in comprehensions 312 + 114 313 // if-else 115 314 ifElse: { 116 315 b: 2 117 316 } 118 317 119 - // for-else with empty list 120 - forElse: { 318 + // for-fallback with empty list 319 + forFallback: { 121 320 empty: true 122 321 } 123 322 124 - // for-else with outer scope reference 125 - forElseOuter: { 126 - fallback: 10 323 + // for-fallback with outer scope reference 324 + forFallbackOuter: { 325 + fallbackField: 10 127 326 } 128 327 } 129 328 == Eval 130 329 { 330 + @experiment(try) 331 + 332 + // Test export of fallback clause in comprehensions 131 333 ifElse: { 132 334 b: 2 133 335 } 134 - forElse: { 336 + forFallback: { 135 337 empty: true 136 338 } 137 - forElseOuter: { 138 - fallback: 10 339 + forFallbackOuter: { 340 + fallbackField: 10 139 341 } 140 342 }
+2 -2
internal/core/walk/walk.go
··· 167 167 w.node(c) 168 168 } 169 169 w.node(adt.ToExpr(x.Value)) 170 - if x.Else != nil { 171 - w.node(x.Else) 170 + if x.Fallback != nil { 171 + w.node(x.Fallback) 172 172 } 173 173 174 174 case *adt.ForClause: