Skip to content

Commit 74788df

Browse files
committed
Add case-when expression support
1 parent b84d5f2 commit 74788df

9 files changed

Lines changed: 242 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ Goal: improve language ergonomics for complex script logic and recovery behavior
212212
- [x] Add `while` loops.
213213
- [x] Add `until` loops.
214214
- [x] Add loop control keywords: `break` and `next`.
215-
- [ ] Add `case/when` expression support (if approved).
215+
- [x] Add `case/when` expression support (if approved).
216216
- [x] Define behavior for nested loop control and block boundaries.
217217

218218
### Error Handling Constructs

docs/control-flow.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
VibeScript supports these control-flow forms:
44

55
- `if` / `elsif` / `else`
6+
- `case` / `when` expressions
67
- `for` loops over arrays and ranges
78
- `while` and `until` loops
89
- loop control with `break` and `next`
@@ -20,6 +21,23 @@ def sum_first_five()
2021
end
2122
```
2223

24+
## `case` / `when` expressions
25+
26+
`case` evaluates to the matching branch expression (or `nil` when no branch matches and no `else` is provided).
27+
28+
```vibe
29+
def label(score)
30+
case score
31+
when 100
32+
"perfect"
33+
when 90, 95
34+
"great"
35+
else
36+
"ok"
37+
end
38+
end
39+
```
40+
2341
## `while` and `until`
2442

2543
```vibe
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
def label(score)
2+
case score
3+
when 100
4+
"perfect"
5+
when 90, 95
6+
"great"
7+
else
8+
"ok"
9+
end
10+
end
11+
12+
def run()
13+
[label(100), label(95), label(70)]
14+
end

vibes/ast.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,21 @@ type RangeExpr struct {
308308
func (e *RangeExpr) exprNode() {}
309309
func (e *RangeExpr) Pos() Position { return e.position }
310310

311+
type CaseWhenClause struct {
312+
Values []Expression
313+
Result Expression
314+
}
315+
316+
type CaseExpr struct {
317+
Target Expression
318+
Clauses []CaseWhenClause
319+
ElseExpr Expression
320+
position Position
321+
}
322+
323+
func (e *CaseExpr) exprNode() {}
324+
func (e *CaseExpr) Pos() Position { return e.position }
325+
311326
type BlockLiteral struct {
312327
Params []Param
313328
Body []Statement

vibes/execution.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,8 @@ func (exec *Execution) evalExpressionWithAuto(expr Expression, env *Env, autoCal
555555
return exec.evalBinaryExpr(e, env)
556556
case *RangeExpr:
557557
return exec.evalRangeExpr(e, env)
558+
case *CaseExpr:
559+
return exec.evalCaseExpr(e, env)
558560
case *MemberExpr:
559561
obj, err := exec.evalExpressionWithAuto(e.Object, env, true)
560562
if err != nil {
@@ -1102,6 +1104,57 @@ func (exec *Execution) evalRangeExpr(expr *RangeExpr, env *Env) (Value, error) {
11021104
return NewRange(Range{Start: start, End: end}), nil
11031105
}
11041106

1107+
func (exec *Execution) evalCaseExpr(expr *CaseExpr, env *Env) (Value, error) {
1108+
target, err := exec.evalExpression(expr.Target, env)
1109+
if err != nil {
1110+
return NewNil(), err
1111+
}
1112+
if err := exec.checkMemoryWith(target); err != nil {
1113+
return NewNil(), err
1114+
}
1115+
1116+
for _, clause := range expr.Clauses {
1117+
matched := false
1118+
for _, candidateExpr := range clause.Values {
1119+
candidate, err := exec.evalExpression(candidateExpr, env)
1120+
if err != nil {
1121+
return NewNil(), err
1122+
}
1123+
if err := exec.checkMemoryWith(candidate); err != nil {
1124+
return NewNil(), err
1125+
}
1126+
if target.Equal(candidate) {
1127+
matched = true
1128+
break
1129+
}
1130+
}
1131+
if !matched {
1132+
continue
1133+
}
1134+
result, err := exec.evalExpressionWithAuto(clause.Result, env, true)
1135+
if err != nil {
1136+
return NewNil(), err
1137+
}
1138+
if err := exec.checkMemoryWith(result); err != nil {
1139+
return NewNil(), err
1140+
}
1141+
return result, nil
1142+
}
1143+
1144+
if expr.ElseExpr != nil {
1145+
result, err := exec.evalExpressionWithAuto(expr.ElseExpr, env, true)
1146+
if err != nil {
1147+
return NewNil(), err
1148+
}
1149+
if err := exec.checkMemoryWith(result); err != nil {
1150+
return NewNil(), err
1151+
}
1152+
return result, nil
1153+
}
1154+
1155+
return NewNil(), nil
1156+
}
1157+
11051158
func (exec *Execution) evalForStatement(stmt *ForStmt, env *Env) (Value, bool, error) {
11061159
exec.loopDepth++
11071160
defer func() {

vibes/lexer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ func lookupIdent(ident string) TokenType {
427427
return tokenIn
428428
case "if":
429429
return tokenIf
430+
case "case":
431+
return tokenCase
432+
case "when":
433+
return tokenWhen
430434
case "elsif":
431435
return tokenElsif
432436
case "else":

vibes/parser.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func newParser(input string) *parser {
6666
p.registerPrefix(tokenBang, p.parsePrefixExpression)
6767
p.registerPrefix(tokenMinus, p.parsePrefixExpression)
6868
p.registerPrefix(tokenYield, p.parseYieldExpression)
69+
p.registerPrefix(tokenCase, p.parseCaseExpression)
6970

7071
p.infixFns[tokenPlus] = p.parseInfixExpression
7172
p.infixFns[tokenMinus] = p.parseInfixExpression
@@ -659,6 +660,66 @@ func (p *parser) parseYieldExpression() Expression {
659660
return &YieldExpr{Args: args, position: pos}
660661
}
661662

663+
func (p *parser) parseCaseExpression() Expression {
664+
pos := p.curToken.Pos
665+
p.nextToken()
666+
target := p.parseExpression(lowestPrec)
667+
if target == nil {
668+
return nil
669+
}
670+
671+
p.nextToken()
672+
clauses := []CaseWhenClause{}
673+
for p.curToken.Type == tokenWhen {
674+
p.nextToken()
675+
values := []Expression{}
676+
first := p.parseExpression(lowestPrec)
677+
if first == nil {
678+
return nil
679+
}
680+
values = append(values, first)
681+
for p.peekToken.Type == tokenComma {
682+
p.nextToken()
683+
p.nextToken()
684+
value := p.parseExpression(lowestPrec)
685+
if value == nil {
686+
return nil
687+
}
688+
values = append(values, value)
689+
}
690+
691+
p.nextToken()
692+
result := p.parseExpressionWithBlock()
693+
if result == nil {
694+
return nil
695+
}
696+
clauses = append(clauses, CaseWhenClause{Values: values, Result: result})
697+
p.nextToken()
698+
}
699+
700+
if len(clauses) == 0 {
701+
p.errorExpected(p.curToken, "when")
702+
return nil
703+
}
704+
705+
var elseExpr Expression
706+
if p.curToken.Type == tokenElse {
707+
p.nextToken()
708+
elseExpr = p.parseExpressionWithBlock()
709+
if elseExpr == nil {
710+
return nil
711+
}
712+
p.nextToken()
713+
}
714+
715+
if p.curToken.Type != tokenEnd {
716+
p.errorExpected(p.curToken, "end")
717+
return nil
718+
}
719+
720+
return &CaseExpr{Target: target, Clauses: clauses, ElseExpr: elseExpr, position: pos}
721+
}
722+
662723
func (p *parser) parseGroupedExpression() Expression {
663724
p.nextToken()
664725
expr := p.parseExpression(lowestPrec)

vibes/runtime_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,80 @@ func TestUntilLoops(t *testing.T) {
862862
}
863863
}
864864

865+
func TestCaseWhenExpressions(t *testing.T) {
866+
script := compileScript(t, `
867+
def label(score)
868+
case score
869+
when 100
870+
"perfect"
871+
when 90, 95
872+
"great"
873+
else
874+
"ok"
875+
end
876+
end
877+
878+
def classify(value)
879+
case value
880+
when nil
881+
"missing"
882+
when true
883+
"yes"
884+
else
885+
"other"
886+
end
887+
end
888+
889+
def assign_case(v)
890+
result = case v
891+
when 1
892+
10
893+
else
894+
20
895+
end
896+
result
897+
end
898+
899+
def unmatched(v)
900+
case v
901+
when 1
902+
"one"
903+
end
904+
end
905+
`)
906+
907+
if got := callFunc(t, script, "label", []Value{NewInt(100)}); !got.Equal(NewString("perfect")) {
908+
t.Fatalf("label(100) mismatch: %v", got)
909+
}
910+
if got := callFunc(t, script, "label", []Value{NewInt(95)}); !got.Equal(NewString("great")) {
911+
t.Fatalf("label(95) mismatch: %v", got)
912+
}
913+
if got := callFunc(t, script, "label", []Value{NewInt(70)}); !got.Equal(NewString("ok")) {
914+
t.Fatalf("label(70) mismatch: %v", got)
915+
}
916+
917+
if got := callFunc(t, script, "classify", []Value{NewNil()}); !got.Equal(NewString("missing")) {
918+
t.Fatalf("classify(nil) mismatch: %v", got)
919+
}
920+
if got := callFunc(t, script, "classify", []Value{NewBool(true)}); !got.Equal(NewString("yes")) {
921+
t.Fatalf("classify(true) mismatch: %v", got)
922+
}
923+
if got := callFunc(t, script, "classify", []Value{NewInt(1)}); !got.Equal(NewString("other")) {
924+
t.Fatalf("classify(1) mismatch: %v", got)
925+
}
926+
927+
if got := callFunc(t, script, "assign_case", []Value{NewInt(1)}); !got.Equal(NewInt(10)) {
928+
t.Fatalf("assign_case(1) mismatch: %v", got)
929+
}
930+
if got := callFunc(t, script, "assign_case", []Value{NewInt(2)}); !got.Equal(NewInt(20)) {
931+
t.Fatalf("assign_case(2) mismatch: %v", got)
932+
}
933+
934+
if got := callFunc(t, script, "unmatched", []Value{NewInt(7)}); !got.Equal(NewNil()) {
935+
t.Fatalf("unmatched(7) expected nil, got %v", got)
936+
}
937+
}
938+
865939
func TestLoopControlBreakAndNext(t *testing.T) {
866940
script := compileScript(t, `
867941
def for_break()

vibes/token.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ const (
6262
tokenNext TokenType = "NEXT"
6363
tokenIn TokenType = "IN"
6464
tokenIf TokenType = "IF"
65+
tokenCase TokenType = "CASE"
66+
tokenWhen TokenType = "WHEN"
6567
tokenElsif TokenType = "ELSIF"
6668
tokenElse TokenType = "ELSE"
6769
tokenTrue TokenType = "TRUE"

0 commit comments

Comments
 (0)