From d9aceeac6edc275c925fe55a10bb60a4198b0379 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 8 Jun 2026 21:08:55 +0300 Subject: [PATCH] feat: switch/case statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- bootstrap/ast.nim | 10 ++++++ bootstrap/hir_lower.nim | 18 ++++++++++ bootstrap/parser.nim | 31 +++++++++++++++++ bootstrap/sema.nim | 8 +++++ bootstrap/token.nim | 11 ++++++- src/ast.bux | 1 + src/hir_lower.bux | 66 +++++++++++++++++++++++++++++++++++++ src/lexer.bux | 3 ++ src/parser.bux | 73 +++++++++++++++++++++++++++++++++++++++++ src/token.bux | 3 ++ 10 files changed, 223 insertions(+), 1 deletion(-) diff --git a/bootstrap/ast.nim b/bootstrap/ast.nim index 09c5fa0..e352716 100644 --- a/bootstrap/ast.nim +++ b/bootstrap/ast.nim @@ -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 diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 322a5c7..bc08ead 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -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) diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index 891af57..d280fe4 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -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 diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 8b89df5..959cfa9 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -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 diff --git a/bootstrap/token.nim b/bootstrap/token.nim index ab582e9..53a7642 100644 --- a/bootstrap/token.nim +++ b/bootstrap/token.nim @@ -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'" diff --git a/src/ast.bux b/src/ast.bux index ab815e2..2d9c357 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -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; diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 90aa950..5ef1567 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -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; } diff --git a/src/lexer.bux b/src/lexer.bux index c4b2bc3..35f5f2f 100644 --- a/src/lexer.bux +++ b/src/lexer.bux @@ -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; } diff --git a/src/parser.bux b/src/parser.bux index ecb3a6a..5bb2620 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -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); diff --git a/src/token.bux b/src/token.bux index f1284d3..e76c30d 100644 --- a/src/token.bux +++ b/src/token.bux @@ -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