feat: switch/case statement

- Add  syntax
- Desugars to if-else chain in HIR lowering (both compilers)
- No new HIR/C backend nodes needed — reuses hIf/hBinary/hBlock
- Supports integer, char, and enum tag dispatch
- Case body can be single statement (no braces required)
This commit is contained in:
2026-06-08 21:08:55 +03:00
parent a67271b08c
commit d9aceeac6e
10 changed files with 223 additions and 1 deletions
+10
View File
@@ -239,6 +239,7 @@ type
skComptime
skEmit
skDefer
skSwitch
skDecl
ElseIf* = object
@@ -246,6 +247,11 @@ type
cond*: Expr
blk*: Block
SwitchCase* = object
loc*: SourceLocation
caseValue*: Expr
caseBody*: Block
Block* = ref object
loc*: SourceLocation
stmts*: seq[Stmt]
@@ -301,6 +307,10 @@ type
stmtEmitEvaluated*: string ## filled by sema CTFE
of skDefer:
stmtDeferBody*: Expr
of skSwitch:
stmtSwitchExpr*: Expr
stmtSwitchCases*: seq[SwitchCase]
stmtSwitchDefault*: Block
of skDecl:
stmtDecl*: Decl
+18
View File
@@ -1127,6 +1127,24 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
return ctx.flushPending(HirNode(kind: hMatch, matchSubject: subject, matchArms: arms,
typ: makeVoid(), loc: loc))
of skSwitch:
let subject = ctx.lowerExpr(stmt.stmtSwitchExpr)
var current: HirNode = nil
# Build if-else chain from bottom up (default first)
if stmt.stmtSwitchDefault != nil:
current = ctx.lowerBlock(stmt.stmtSwitchDefault)
# Cases in reverse order
for i in countdown(stmt.stmtSwitchCases.len - 1, 0):
let caseBranch = stmt.stmtSwitchCases[i]
let caseVal = ctx.lowerExpr(caseBranch.caseValue)
let caseBody = ctx.lowerBlock(caseBranch.caseBody)
let cond = HirNode(kind: hBinary, binaryOp: tkEq,
binaryLeft: subject, binaryRight: caseVal,
typ: makeBool(), loc: caseBranch.loc)
current = HirNode(kind: hIf, ifCond: cond, ifThen: caseBody, ifElse: current,
typ: makeVoid(), loc: caseBranch.loc)
return ctx.flushPending(current)
of skDefer:
let body = ctx.lowerExpr(stmt.stmtDeferBody)
ctx.deferStmts.add(body)
+31
View File
@@ -901,6 +901,37 @@ proc parseStmt(p: var Parser): Stmt =
discard p.advance()
discard p.expect(tkRBrace, "expected '}' to close match")
return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekMatch, loc: loc, exprMatchSubject: subject, exprMatchArms: arms))
of tkSwitch:
discard p.advance()
p.structInitAllowed = false
let subject = p.parseExpr()
p.structInitAllowed = true
while p.check(tkNewLine):
discard p.advance()
discard p.expect(tkLBrace, "expected '{' to start switch body")
var cases: seq[SwitchCase] = @[]
var defaultBlock: Block = nil
while not p.check(tkRBrace) and not p.isAtEnd:
while p.check(tkNewLine):
discard p.advance()
if p.check(tkRBrace) or p.isAtEnd:
break
if p.check(tkDefault):
discard p.advance()
discard p.expect(tkColon, "expected ':' after default")
defaultBlock = Block(loc: p.currentLoc, stmts: @[p.parseStmt()])
continue
if p.check(tkCase):
discard p.advance()
let caseVal = p.parseExpr()
discard p.expect(tkColon, "expected ':' after case value")
let caseBody = Block(loc: p.currentLoc, stmts: @[p.parseStmt()])
cases.add(SwitchCase(loc: caseVal.loc, caseValue: caseVal, caseBody: caseBody))
continue
# Unknown token in switch body — skip
discard p.advance()
discard p.expect(tkRBrace, "expected '}' to close switch")
return Stmt(kind: skSwitch, loc: loc, stmtSwitchExpr: subject, stmtSwitchCases: cases, stmtSwitchDefault: defaultBlock)
of tkReturn:
discard p.advance()
var val: Expr = nil
+8
View File
@@ -1338,6 +1338,14 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
of skDefer:
discard sema.checkExpr(stmt.stmtDeferBody, scope)
return makeVoid()
of skSwitch:
discard sema.checkExpr(stmt.stmtSwitchExpr, scope)
for caseBranch in stmt.stmtSwitchCases:
discard sema.checkExpr(caseBranch.caseValue, scope)
discard sema.checkStmt(Stmt(kind: skExpr, loc: caseBranch.caseBody.loc, stmtExpr: Expr(kind: ekBlock, loc: caseBranch.caseBody.loc, exprBlock: caseBranch.caseBody)), scope)
if stmt.stmtSwitchDefault != nil:
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtSwitchDefault.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtSwitchDefault.loc, exprBlock: stmt.stmtSwitchDefault)), scope)
return makeVoid()
of skDecl:
# Local declaration inside block
case stmt.stmtDecl.kind
+10 -1
View File
@@ -25,6 +25,9 @@ type
tkContinue # continue
tkReturn # return
tkMatch # match
tkSwitch # switch
tkCase # case
tkDefault # default
##Declaration keywords
tkFunc # func
@@ -150,7 +153,7 @@ type
proc isKeyword*(kind: TokenKind): bool =
case kind
of tkIf, tkElse, tkWhile, tkDo, tkLoop, tkFor, tkIn,
tkBreak, tkContinue, tkReturn, tkMatch,
tkBreak, tkContinue, tkReturn, tkMatch, tkSwitch, tkCase, tkDefault,
tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum,
tkUnion, tkInterface, tkExtend, tkModule, tkImport,
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkBorrow, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn, tkDefer:
@@ -201,6 +204,9 @@ proc keywordKind*(text: string): TokenKind =
of "continue": tkContinue
of "return": tkReturn
of "match": tkMatch
of "switch": tkSwitch
of "case": tkCase
of "default": tkDefault
of "as": tkAs
of "is": tkIs
of "null": tkNull
@@ -242,6 +248,9 @@ proc tokenKindName*(kind: TokenKind): string =
of tkContinue: "'continue'"
of tkReturn: "'return'"
of tkMatch: "'match'"
of tkSwitch: "'switch'"
of tkCase: "'case'"
of tkDefault: "'default'"
of tkFunc: "'func'"
of tkLet: "'let'"
of tkVar: "'var'"
+1
View File
@@ -158,6 +158,7 @@ const skBreak: int = 9;
const skContinue: int = 10;
const skDecl: int = 11;
const skDefer: int = 12;
const skSwitch: int = 13;
struct ElseIf {
line: uint32;
+66
View File
@@ -491,6 +491,72 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
return n;
}
// Switch — desugar to if-else chain
if kind == skSwitch {
let subject: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
var current: *HirNode = null as *HirNode;
// Default first (bottom of chain)
if stmt.refStmtElse != null as *Block {
current = Lcx_LowerBlock(ctx, stmt.refStmtElse, -1);
}
// Cases in reverse order (from caseBlock)
if stmt.refStmtBlock != null as *Block {
let caseBlock: *Block = stmt.refStmtBlock;
var caseCount: int = caseBlock.stmtCount;
// Collect cases into array for reverse iteration
var c0: *Stmt = null as *Stmt;
var c1: *Stmt = null as *Stmt;
var c2: *Stmt = null as *Stmt;
var c3: *Stmt = null as *Stmt;
var c4: *Stmt = null as *Stmt;
var c5: *Stmt = null as *Stmt;
var c6: *Stmt = null as *Stmt;
var c7: *Stmt = null as *Stmt;
var ci: int = 0;
var cs: *Stmt = caseBlock.firstStmt;
while cs != null as *Stmt && ci < 8 {
if ci == 0 { c0 = cs; }
if ci == 1 { c1 = cs; }
if ci == 2 { c2 = cs; }
if ci == 3 { c3 = cs; }
if ci == 4 { c4 = cs; }
if ci == 5 { c5 = cs; }
if ci == 6 { c6 = cs; }
if ci == 7 { c7 = cs; }
ci = ci + 1;
cs = cs.nextStmt;
}
while caseCount > 0 {
caseCount = caseCount - 1;
var c: *Stmt = null as *Stmt;
if caseCount == 0 { c = c0; }
if caseCount == 1 { c = c1; }
if caseCount == 2 { c = c2; }
if caseCount == 3 { c = c3; }
if caseCount == 4 { c = c4; }
if caseCount == 5 { c = c5; }
if caseCount == 6 { c = c6; }
if caseCount == 7 { c = c7; }
if c != null as *Stmt {
let caseVal: *HirNode = Lcx_LowerExpr(ctx, c.child1);
let caseBody: *HirNode = Lcx_LowerBlock(ctx, c.refStmtBlock, -1);
let cond: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
cond.kind = hBinary;
cond.intValue = 74; // tkEq
cond.child1 = subject;
cond.child2 = caseVal;
let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
ifNode.kind = hIf;
ifNode.child1 = cond;
ifNode.child2 = caseBody;
ifNode.child3 = current;
current = ifNode;
}
}
}
return current;
}
return n;
}
+3
View File
@@ -302,6 +302,9 @@ func lexKeywordKind(text: String) -> int {
if String_Eq(text, "sizeof") { return tkSizeOf; }
if String_Eq(text, "discard") { return tkDiscard; }
if String_Eq(text, "defer") { return tkDefer; }
if String_Eq(text, "switch") { return tkSwitch; }
if String_Eq(text, "case") { return tkCase; }
if String_Eq(text, "default") { return tkDefault; }
if String_Eq(text, "async") { return tkAsync; }
if String_Eq(text, "await") { return tkAwait; }
if String_Eq(text, "spawn") { return tkSpawn; }
+73
View File
@@ -855,6 +855,79 @@ func parserParseStmt(p: *Parser) -> *Stmt {
return s;
}
// switch
if kind == tkSwitch {
discard parserAdvance(p);
p.structInitAllowed = false;
let subject: *Expr = parserParseExpr(p);
p.structInitAllowed = true;
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
discard parserExpect(p, tkLBrace, "expected '{' to start switch body");
var caseBlock: *Block = bux_alloc(sizeof(Block)) as *Block;
caseBlock.line = line;
caseBlock.column = col;
caseBlock.stmtCount = 0;
caseBlock.firstStmt = null as *Stmt;
caseBlock.lastStmt = null as *Stmt;
var defaultBody: *Block = null as *Block;
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserCheck(p, tkRBrace) || parserPeek(p, 0) == tkEndOfFile { break; }
if parserCheck(p, tkDefault) {
discard parserAdvance(p);
discard parserExpect(p, tkColon, "expected ':' after default");
let defaultStmt: *Stmt = parserParseStmt(p);
defaultBody = bux_alloc(sizeof(Block)) as *Block;
defaultBody.line = defaultStmt.line;
defaultBody.column = defaultStmt.column;
defaultBody.stmtCount = 1;
defaultBody.firstStmt = defaultStmt;
defaultBody.lastStmt = defaultStmt;
defaultStmt.nextStmt = null as *Stmt;
continue;
}
if parserCheck(p, tkCase) {
discard parserAdvance(p);
let caseVal: *Expr = parserParseExpr(p);
discard parserExpect(p, tkColon, "expected ':' after case value");
let caseStmt: *Stmt = parserParseStmt(p);
let caseBody: *Block = bux_alloc(sizeof(Block)) as *Block;
caseBody.line = caseStmt.line;
caseBody.column = caseStmt.column;
caseBody.stmtCount = 1;
caseBody.firstStmt = caseStmt;
caseBody.lastStmt = caseStmt;
caseStmt.nextStmt = null as *Stmt;
let c: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt;
c.kind = skExpr;
c.line = caseVal.line;
c.column = caseVal.column;
c.child1 = caseVal;
c.refStmtBlock = caseBody;
c.nextStmt = null as *Stmt;
if caseBlock.firstStmt == null as *Stmt {
caseBlock.firstStmt = c;
caseBlock.lastStmt = c;
} else {
caseBlock.lastStmt.nextStmt = c;
caseBlock.lastStmt = c;
}
caseBlock.stmtCount = caseBlock.stmtCount + 1;
continue;
}
discard parserAdvance(p);
}
discard parserExpect(p, tkRBrace, "expected '}' to close switch");
let s: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt;
s.kind = skSwitch;
s.line = line;
s.column = col;
s.child1 = subject;
s.refStmtBlock = caseBlock;
s.refStmtElse = defaultBody;
return s;
}
// Expression statement
if kind == tkNewLine || kind == tkSemicolon {
discard parserAdvance(p);
+3
View File
@@ -139,6 +139,9 @@ const tkAwait: int = 103;
const tkSpawn: int = 104;
const tkDiscard: int = 105;
const tkDefer: int = 106;
const tkSwitch: int = 107;
const tkCase: int = 108;
const tkDefault: int = 109;
// ---------------------------------------------------------------------------
// Token struct