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 skComptime
skEmit skEmit
skDefer skDefer
skSwitch
skDecl skDecl
ElseIf* = object ElseIf* = object
@@ -246,6 +247,11 @@ type
cond*: Expr cond*: Expr
blk*: Block blk*: Block
SwitchCase* = object
loc*: SourceLocation
caseValue*: Expr
caseBody*: Block
Block* = ref object Block* = ref object
loc*: SourceLocation loc*: SourceLocation
stmts*: seq[Stmt] stmts*: seq[Stmt]
@@ -301,6 +307,10 @@ type
stmtEmitEvaluated*: string ## filled by sema CTFE stmtEmitEvaluated*: string ## filled by sema CTFE
of skDefer: of skDefer:
stmtDeferBody*: Expr stmtDeferBody*: Expr
of skSwitch:
stmtSwitchExpr*: Expr
stmtSwitchCases*: seq[SwitchCase]
stmtSwitchDefault*: Block
of skDecl: of skDecl:
stmtDecl*: Decl 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, return ctx.flushPending(HirNode(kind: hMatch, matchSubject: subject, matchArms: arms,
typ: makeVoid(), loc: loc)) 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: of skDefer:
let body = ctx.lowerExpr(stmt.stmtDeferBody) let body = ctx.lowerExpr(stmt.stmtDeferBody)
ctx.deferStmts.add(body) ctx.deferStmts.add(body)
+31
View File
@@ -901,6 +901,37 @@ proc parseStmt(p: var Parser): Stmt =
discard p.advance() discard p.advance()
discard p.expect(tkRBrace, "expected '}' to close match") discard p.expect(tkRBrace, "expected '}' to close match")
return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekMatch, loc: loc, exprMatchSubject: subject, exprMatchArms: arms)) 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: of tkReturn:
discard p.advance() discard p.advance()
var val: Expr = nil var val: Expr = nil
+8
View File
@@ -1338,6 +1338,14 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
of skDefer: of skDefer:
discard sema.checkExpr(stmt.stmtDeferBody, scope) discard sema.checkExpr(stmt.stmtDeferBody, scope)
return makeVoid() 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: of skDecl:
# Local declaration inside block # Local declaration inside block
case stmt.stmtDecl.kind case stmt.stmtDecl.kind
+10 -1
View File
@@ -25,6 +25,9 @@ type
tkContinue # continue tkContinue # continue
tkReturn # return tkReturn # return
tkMatch # match tkMatch # match
tkSwitch # switch
tkCase # case
tkDefault # default
##Declaration keywords ##Declaration keywords
tkFunc # func tkFunc # func
@@ -150,7 +153,7 @@ type
proc isKeyword*(kind: TokenKind): bool = proc isKeyword*(kind: TokenKind): bool =
case kind case kind
of tkIf, tkElse, tkWhile, tkDo, tkLoop, tkFor, tkIn, 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, tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum,
tkUnion, tkInterface, tkExtend, tkModule, tkImport, tkUnion, tkInterface, tkExtend, tkModule, tkImport,
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkBorrow, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn, tkDefer: 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 "continue": tkContinue
of "return": tkReturn of "return": tkReturn
of "match": tkMatch of "match": tkMatch
of "switch": tkSwitch
of "case": tkCase
of "default": tkDefault
of "as": tkAs of "as": tkAs
of "is": tkIs of "is": tkIs
of "null": tkNull of "null": tkNull
@@ -242,6 +248,9 @@ proc tokenKindName*(kind: TokenKind): string =
of tkContinue: "'continue'" of tkContinue: "'continue'"
of tkReturn: "'return'" of tkReturn: "'return'"
of tkMatch: "'match'" of tkMatch: "'match'"
of tkSwitch: "'switch'"
of tkCase: "'case'"
of tkDefault: "'default'"
of tkFunc: "'func'" of tkFunc: "'func'"
of tkLet: "'let'" of tkLet: "'let'"
of tkVar: "'var'" of tkVar: "'var'"
+1
View File
@@ -158,6 +158,7 @@ const skBreak: int = 9;
const skContinue: int = 10; const skContinue: int = 10;
const skDecl: int = 11; const skDecl: int = 11;
const skDefer: int = 12; const skDefer: int = 12;
const skSwitch: int = 13;
struct ElseIf { struct ElseIf {
line: uint32; line: uint32;
+66
View File
@@ -491,6 +491,72 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
return n; 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; 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, "sizeof") { return tkSizeOf; }
if String_Eq(text, "discard") { return tkDiscard; } if String_Eq(text, "discard") { return tkDiscard; }
if String_Eq(text, "defer") { return tkDefer; } 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, "async") { return tkAsync; }
if String_Eq(text, "await") { return tkAwait; } if String_Eq(text, "await") { return tkAwait; }
if String_Eq(text, "spawn") { return tkSpawn; } if String_Eq(text, "spawn") { return tkSpawn; }
+73
View File
@@ -855,6 +855,79 @@ func parserParseStmt(p: *Parser) -> *Stmt {
return s; 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 // Expression statement
if kind == tkNewLine || kind == tkSemicolon { if kind == tkNewLine || kind == tkSemicolon {
discard parserAdvance(p); discard parserAdvance(p);
+3
View File
@@ -139,6 +139,9 @@ const tkAwait: int = 103;
const tkSpawn: int = 104; const tkSpawn: int = 104;
const tkDiscard: int = 105; const tkDiscard: int = 105;
const tkDefer: int = 106; const tkDefer: int = 106;
const tkSwitch: int = 107;
const tkCase: int = 108;
const tkDefault: int = 109;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Token struct // Token struct