feat: macros (multi-rep, hygiene), Drop field-move, lean multi-OS CI
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled

Sessions 56–69: declarative macro! with rep/zip/literal/block and
unhygienic var $name binders; partial field-move skip Drop; @[Release]
polish; LSP type hierarchy; CI Nim cache + lean macOS + Windows smoke.
This commit is contained in:
2026-07-20 17:19:46 +03:00
parent 6f2a3b1d88
commit fe3b1e8b6a
41 changed files with 5281 additions and 141 deletions
+35
View File
@@ -133,6 +133,7 @@ type
ekMatch
ekStringInterp
ekClosure
ekMacroCall ## name!(args) — expanded before sema
MatchArm* = object
loc*: SourceLocation
@@ -236,6 +237,12 @@ type
captureCount*: int
captureNames*: seq[string]
captureTypeKinds*: seq[int]
of ekMacroCall:
exprMacroName*: string
exprMacroArgs*: seq[Expr]
## Group lengths for multi-rep: `m!(1,2; 3,4)` → @[2, 2].
## Empty means a single group of all args.
exprMacroGroupLens*: seq[int]
# ---------------------------------------------------------------------------
# Statements
@@ -258,6 +265,7 @@ type
skDefer
skSwitch
skDecl
skMacroRep ## $( … )* template repetition (macro body only)
ElseIf* = object
loc*: SourceLocation
@@ -330,6 +338,8 @@ type
stmtSwitchDefault*: Block
of skDecl:
stmtDecl*: Decl
of skMacroRep: ## $( stmts… )* in macro templates
stmtMacroRepBody*: Block
# ---------------------------------------------------------------------------
# Type Parameters (for generics with trait bounds)
@@ -356,6 +366,28 @@ type
dkExternFunc
dkExternVar
dkExternBlock
dkMacro ## macro! name { ($x:expr) => { … } }
## One declarative macro arm: ($a:expr, $($x:expr),*) => { template }
MacroFragKind* = enum
mfkExpr ## any expression
mfkIdent ## bare identifier (after expand must be ekIdent)
mfkTt ## token-tree (MVP: same as expr)
mfkLiteral ## int/float/string/char/bool literal only
mfkBlock ## block expression `{ … }`
MacroFragment* = object
name*: string ## primary / first name (compat)
kind*: MacroFragKind ## primary kind (compat)
names*: seq[string] ## one or more $names (compound rep: $a,$b)
kinds*: seq[MacroFragKind] ## parallel to names
isRep*: bool ## true for $( … ),* or $( … )*
repSep*: string ## "," if separator was present before *, else ""
MacroRule* = object
loc*: SourceLocation
frags*: seq[MacroFragment]
body*: Block ## template (substituted, then used as ekBlock)
Param* = object
loc*: SourceLocation
@@ -448,6 +480,9 @@ type
declExtBlockDll*: string
declExtBlockCallConv*: CallingConvention
declExtBlockItems*: seq[Decl]
of dkMacro:
declMacroName*: string
declMacroRules*: seq[MacroRule]
# ---------------------------------------------------------------------------
# Module (AST root)
+19 -1
View File
@@ -4,6 +4,7 @@ import source_location
import fmt
import docgen
import registry
import macroexpand
type
ColorMode* = enum
@@ -653,6 +654,12 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
if status != 0:
return status
let unifiedModule = mergeProject(pctx)
let macRes = expandMacros(unifiedModule)
if macRes.diagnostics.len > 0:
printError("macro expansion errors", useColor)
for d in macRes.diagnostics:
printDiagnostic("error", d.message, d.loc, useColor)
return 1
let semaRes = analyze(unifiedModule)
if semaRes.hasErrors:
printError("type errors in project", useColor)
@@ -689,6 +696,7 @@ proc getDeclName(d: Decl): string =
of dkInterface: d.declInterfaceName
of dkConst: d.declConstName
of dkTypeAlias: d.declAliasName
of dkMacro: d.declMacroName
else: ""
proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl] =
@@ -763,6 +771,14 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let unifiedModule = mergeProject(pctx)
# Phase 2b: expand declarative macro! / quote! before type checking
let macRes = expandMacros(unifiedModule)
if macRes.diagnostics.len > 0:
printError("macro expansion errors", useColor)
for d in macRes.diagnostics:
printDiagnostic("error", d.message, d.loc, useColor)
return 1
# Phase 3: Sema + HIR + C codegen on unified module
let (semaRes, semaCtx) = analyzeFull(unifiedModule)
if semaRes.hasErrors:
@@ -809,7 +825,9 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g"
let extraCflags = getEnv("BUX_CFLAGS")
let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags
let ccCmd = &"cc {cflags} -pthread -Wl,--build-id=none -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1"
# --build-id is GNU ld only (breaks Apple ld). Reproducible selfhost-loop uses Linux CI.
let ldStable = when defined(linux): " -Wl,--build-id=none" else: ""
let ccCmd = &"cc {cflags} -pthread{ldStable} -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1"
if opts.verbose:
printInfo(&"running: {ccCmd}", useColor)
let (output, exitCode) = execCmdEx(ccCmd)
+32 -1
View File
@@ -85,12 +85,23 @@ proc markMovedOutLocal(ctx: var LowerCtx, name: string) =
if name.len > 0 and ctx.hasPendingDrop(name):
ctx.movedOutLocals.incl(name)
# Forward decls (used by markMovedOutFromAst before their full definitions)
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string
proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
## Mark droppable locals used by-value in ownership-taking contexts.
## Partial field moves: `return bag.items` / `let x = bag.items` mark `bag`
## so auto-Drop of the parent is skipped — **only when the field type itself
## is droppable** (not `return bag.tag` for an int field).
if expr == nil: return
case expr.kind
of ekIdent:
ctx.markMovedOutLocal(expr.exprIdent)
of ekField:
let fieldTy = ctx.resolveExprType(expr)
if ctx.autoDropFuncName(fieldTy).len > 0:
ctx.markMovedOutFromAst(expr.exprFieldObj)
of ekStructInit:
for f in expr.exprStructInitFields:
ctx.markMovedOutFromAst(f.value)
@@ -2131,6 +2142,11 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
of skMacroRep:
# Expanded before lowering
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
## asExpr=true: block is used as a value (`let x = { ... }`, match arm body).
## Last skExpr becomes the block result. Statement blocks (func body, if/while)
@@ -2168,15 +2184,30 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
expr = last.blockExpr
# Scope exit: Drop locals introduced in this block (not outer ones).
# Skip Drop for block result and any moved-out locals (field / let / return move).
# If the last statement always returns, drops were already injected on that
# path — re-emitting them here produces dead double-Drop after `return`.
proc blockAlwaysReturns(n: HirNode): bool =
if n == nil: return false
if n.kind == hReturn: return true
if n.kind == hBlock:
if n.blockStmts.len == 0: return false
return blockAlwaysReturns(n.blockStmts[^1])
false
var skipDrop = ""
if expr != nil and expr.kind == hVar:
skipDrop = expr.varName
ctx.markMovedOutLocal(expr.varName)
if ctx.deferStmts.len > deferBase:
let lastAlwaysReturns = stmts.len > 0 and blockAlwaysReturns(stmts[^1])
if ctx.deferStmts.len > deferBase and not lastAlwaysReturns:
for i in countdown(ctx.deferStmts.len - 1, deferBase):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i])
ctx.deferStmts.setLen(deferBase)
elif ctx.deferStmts.len > deferBase and lastAlwaysReturns:
# Return path already owns these drops; pop so outer scopes don't re-run them
# for the same locals when this block is nested. Outer live locals remain.
ctx.deferStmts.setLen(deferBase)
let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid()
return hirBlock(stmts, expr, typ, blk.loc, isScope = true)
+11
View File
@@ -464,6 +464,17 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
return lex.makeToken(tkCaretAssign, startLoc, startPos)
else:
return lex.makeToken(tkCaret, startLoc, startPos)
of '$':
# $name fragment, or bare $ for macro repetition $( ... )*
if isIdentStart(lex.peek()):
discard lex.advance() # first ident char ( $ already consumed as c1)
while not lex.isAtEnd() and isIdentChar(lex.peek()):
discard lex.advance()
# text includes leading '$'
return lex.makeToken(tkIdent, startLoc, startPos)
else:
# bare $ (c1 already consumed)
return lex.makeToken(tkDollar, startLoc, startPos)
of '#':
# Check for intrinsics: #line, #column, #file, #function, #date, #time, #module
let afterHash = lex.peek()
+5 -4
View File
@@ -120,14 +120,15 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
of lirShl: "<<"
of lirShr: ">>"
else: "?"
be.emitLine(&"{v(instr.dst)} = {v(instr.src)} {op} {v(instr.src2)};")
# Parenthesize so future non-temp operands cannot be rewritten by C precedence
be.emitLine(&"{v(instr.dst)} = ({v(instr.src)} {op} {v(instr.src2)});")
of lirNeg:
be.emitLine(&"{v(instr.dst)} = -{v(instr.src)};")
be.emitLine(&"{v(instr.dst)} = -({v(instr.src)});")
of lirNot:
be.emitLine(&"{v(instr.dst)} = !{v(instr.src)};")
be.emitLine(&"{v(instr.dst)} = !({v(instr.src)});")
of lirBNot:
be.emitLine(&"{v(instr.dst)} = ~{v(instr.src)};")
be.emitLine(&"{v(instr.dst)} = ~({v(instr.src)});")
# ── Comparison ──
of lirCmpEq, lirCmpNe, lirCmpLt, lirCmpLe, lirCmpGt, lirCmpGe:
File diff suppressed because it is too large Load Diff
+204 -3
View File
@@ -21,12 +21,14 @@ type
pos: int
diagnostics: seq[ParserDiagnostic]
structInitAllowed: bool ## disabled inside if/while/for/match conditions
macroTemplateMode: bool ## true while parsing macro! rule body (allows $(…)*)
proc initParser*(tokens: seq[Token], sourceName: string = "<input>"): Parser =
result.tokens = tokens
result.sourceName = sourceName
result.pos = 0
result.structInitAllowed = true
result.macroTemplateMode = false
# ---------------------------------------------------------------------------
# Token helpers
@@ -152,7 +154,7 @@ proc synchronize(p: var Parser) =
if p.previous.kind == tkSemicolon: return
case p.peek()
of tkFunc, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend,
tkModule, tkImport, tkConst, tkType, tkExtern, tkPub:
tkModule, tkImport, tkConst, tkType, tkExtern, tkPub, tkMacro:
return
else:
discard p.advance()
@@ -183,7 +185,10 @@ type
release*: bool ## @[Release] — explicit zero-cost (no borrow checks)
proc parseAttrs(p: var Parser): ParsedAttrs =
while p.check(tkAt):
while true:
p.skipNewlines()
if not p.check(tkAt):
break
discard p.advance() # @
discard p.expect(tkLBracket, "expected '[' after '@'")
let name = p.expect(tkIdent, "expected attribute name").text
@@ -728,7 +733,43 @@ proc parsePostfix(p: var Parser): Expr =
left = Expr(kind: ekTry, loc: loc, exprTryOperand: left, exprTryType: nil)
of tkBang:
discard p.advance()
left = Expr(kind: ekUnwrap, loc: loc, exprUnwrapOperand: left)
# name!(args) → declarative macro call (not unwrap)
if left.kind == ekIdent and p.check(tkLParen):
discard p.advance() # (
var margs: seq[Expr] = @[]
var groupLens: seq[int] = @[]
var curGroup = 0
while not p.check(tkRParen) and not p.isAtEnd:
p.skipNewlines()
if p.check(tkRParen): break
# `;` starts a new arg group for multi-rep patterns
if p.check(tkSemicolon):
discard p.advance()
groupLens.add(curGroup)
curGroup = 0
p.skipNewlines()
continue
margs.add(p.parseExpr())
inc curGroup
p.skipNewlines()
if p.check(tkComma):
discard p.advance()
elif p.check(tkSemicolon):
discard
# handled at loop top
else:
# allow end of args
discard
if curGroup > 0 or groupLens.len == 0:
groupLens.add(curGroup)
# single group of all args → empty groupLens means "one group" for expander
if groupLens.len == 1:
groupLens = @[]
discard p.expect(tkRParen, "expected ')' to close macro arguments")
left = Expr(kind: ekMacroCall, loc: loc, exprMacroName: left.exprIdent,
exprMacroArgs: margs, exprMacroGroupLens: groupLens)
else:
left = Expr(kind: ekUnwrap, loc: loc, exprUnwrapOperand: left)
of tkLBrace:
if p.structInitAllowed and left.kind in {ekIdent, ekPath, ekGenericCall}:
discard p.advance()
@@ -952,7 +993,26 @@ proc parseBlock(p: var Parser): Block =
# ---------------------------------------------------------------------------
proc parseStmt(p: var Parser): Stmt =
while p.check(tkNewLine):
discard p.advance()
let loc = p.currentLoc
# Macro template repetition: $( stmts… )*
if p.macroTemplateMode and p.check(tkDollar) and p.peek(1) == tkLParen:
discard p.advance() # $
discard p.advance() # (
var stmts: seq[Stmt] = @[]
while not p.check(tkRParen) and not p.isAtEnd:
while p.check(tkNewLine):
discard p.advance()
if p.check(tkRParen) or p.isAtEnd:
break
stmts.add(p.parseStmt())
discard p.expect(tkRParen, "expected ')' to close macro repetition")
discard p.expect(tkStar, "expected '*' after macro repetition")
if p.check(tkSemicolon):
discard p.advance()
return Stmt(kind: skMacroRep, loc: loc,
stmtMacroRepBody: Block(loc: loc, stmts: stmts))
case p.peek()
of tkLet, tkVar:
let isMut = p.peek() == tkVar
@@ -1561,6 +1621,145 @@ proc parseExternDecl(p: var Parser, isPublic: bool, attrs: ParsedAttrs): Decl =
return Decl(kind: dkExternVar, loc: loc, isPublic: isPublic,
declExtVarName: vName, declExtVarType: vType)
proc parseMacroFragKind(p: var Parser, kindTok: Token): MacroFragKind =
case kindTok.text
of "expr": mfkExpr
of "ident": mfkIdent
of "tt": mfkTt
of "literal", "lit": mfkLiteral
of "block": mfkBlock
else:
p.emitError(kindTok.loc,
"unsupported macro fragment kind '" & kindTok.text &
"' (expr|ident|tt|literal|block)")
mfkExpr
proc parseMacroFragment(p: var Parser): MacroFragment =
## $name:kind (single non-rep fragment)
let fragTok = p.expect(tkIdent, "expected $name fragment in macro pattern")
if not fragTok.text.startsWith("$"):
p.emitError(fragTok.loc, "macro fragment must start with '$' (e.g. $x:expr)")
discard p.expect(tkColon, "expected ':' after macro fragment name")
let kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block)")
let k = p.parseMacroFragKind(kindTok)
result = MacroFragment(
name: fragTok.text,
kind: k,
names: @[fragTok.text],
kinds: @[k],
isRep: false,
repSep: "")
proc parseMacroRepGroup(p: var Parser): MacroFragment =
## $( $a:kind , $b:kind , … ) ,* or … )*
## Compound: multiple frags inside one rep → parallel lists (zipped).
discard p.expect(tkDollar, "expected '$'")
discard p.expect(tkLParen, "expected '(' after '$'")
p.skipNewlines()
var names: seq[string] = @[]
var kinds: seq[MacroFragKind] = @[]
while not p.check(tkRParen) and not p.isAtEnd:
let fragTok = p.expect(tkIdent, "expected $name inside repetition")
if not fragTok.text.startsWith("$"):
p.emitError(fragTok.loc, "macro fragment must start with '$'")
discard p.expect(tkColon, "expected ':' after fragment name")
let kindTok = p.expect(tkIdent, "expected fragment kind")
names.add(fragTok.text)
kinds.add(p.parseMacroFragKind(kindTok))
p.skipNewlines()
if p.check(tkComma):
discard p.advance()
p.skipNewlines()
else:
break
if names.len == 0:
p.emitError(p.currentLoc, "empty macro repetition group")
names.add("$x")
kinds.add(mfkExpr)
discard p.expect(tkRParen, "expected ')' after repeated fragment(s)")
var sep = ""
if p.check(tkComma):
discard p.advance()
sep = ","
discard p.expect(tkStar, "expected '*' after macro repetition")
result = MacroFragment(
name: names[0],
kind: kinds[0],
names: names,
kinds: kinds,
isRep: true,
repSep: sep)
proc parseMacroDecl(p: var Parser, isPublic: bool): Decl =
## macro! name {
## ( $a:ident, $($x:expr),* ) => { … }
## ( $($a:expr, $b:expr),* ) => { … } # compound / zipped
## ( $($x:expr),* ; $($y:expr),* ) => { … } # multi-rep groups
## }
let loc = p.currentLoc
discard p.expect(tkMacro, "expected 'macro'")
discard p.expect(tkBang, "expected '!' after macro")
let name = p.expect(tkIdent, "expected macro name").text
p.skipNewlines()
discard p.expect(tkLBrace, "expected '{' to start macro body")
var rules: seq[MacroRule] = @[]
while not p.check(tkRBrace) and not p.isAtEnd:
p.skipNewlines()
if p.check(tkRBrace) or p.isAtEnd:
break
let rloc = p.currentLoc
discard p.expect(tkLParen, "expected '(' to start macro pattern")
var frags: seq[MacroFragment] = @[]
while not p.check(tkRParen) and not p.isAtEnd:
p.skipNewlines()
if p.check(tkRParen): break
# Group separator for multi-rep: `;` between pattern elements
if p.check(tkSemicolon):
discard p.advance()
p.skipNewlines()
continue
# $( … ),* compound or single rep
if p.check(tkDollar) and p.peek(1) == tkLParen:
frags.add(p.parseMacroRepGroup())
p.skipNewlines()
# optional `;` after rep continues with more elements
if p.check(tkSemicolon):
discard p.advance()
p.skipNewlines()
continue
if p.check(tkComma):
discard p.advance()
p.skipNewlines()
continue
# no more separators → only rparen expected next
break
else:
frags.add(p.parseMacroFragment())
p.skipNewlines()
if p.check(tkComma):
discard p.advance()
elif p.check(tkSemicolon):
discard p.advance()
else:
break
discard p.expect(tkRParen, "expected ')' to close macro pattern")
p.skipNewlines()
discard p.expect(tkFatArrow, "expected '=>' after macro pattern")
p.skipNewlines()
let savedTpl = p.macroTemplateMode
p.macroTemplateMode = true
let body = p.parseBlock()
p.macroTemplateMode = savedTpl
rules.add(MacroRule(loc: rloc, frags: frags, body: body))
p.skipNewlines()
if p.check(tkComma) or p.check(tkSemicolon):
discard p.advance()
discard p.expect(tkRBrace, "expected '}' to close macro")
if rules.len == 0:
p.emitError(loc, "macro '" & name & "' has no rules")
return Decl(kind: dkMacro, loc: loc, isPublic: isPublic,
declMacroName: name, declMacroRules: rules)
proc parseDecl(p: var Parser): Decl =
let loc = p.currentLoc
var isPublic = false
@@ -1606,6 +1805,8 @@ proc parseDecl(p: var Parser): Decl =
return p.parseTypeAliasDecl(isPublic)
of tkExtern:
return p.parseExternDecl(isPublic, attrs)
of tkMacro:
return p.parseMacroDecl(isPublic)
else:
p.emitError(loc, "expected declaration")
p.synchronize()
+15 -2
View File
@@ -41,7 +41,8 @@ type
# Interface name -> interface decl
interfaceTable*: Table[string, Decl]
# Borrow checker state
checkedFunc*: bool ## true inside @[Checked] function
checkedFunc*: bool ## true inside @[Checked] and not @[Release]
releaseFunc*: bool ## true inside @[Release] (zero-cost: no borrow checks)
currentFuncIsAsync*: bool ## true inside async func
movedVars*: seq[string] ## variables moved in current checked function
## Active exclusive borrows: source var → borrow site (let-bound &mut lasts for rest of fn)
@@ -1897,6 +1898,10 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
for e in expr.exprInterpExprs:
discard sema.checkExpr(e, scope)
return makeStr()
of ekMacroCall:
# Should have been expanded before analyze; leftover is a compiler bug
sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'")
return makeUnknown()
of ekClosure:
let savedRetType = sema.currentRetType
let savedClosureDepth = sema.closureDepth
@@ -2088,6 +2093,10 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
else:
discard
return makeVoid()
of skMacroRep:
# Templates with $(…)* must be expanded before type-check
sema.emitError(stmt.loc, "unexpanded macro repetition '$(…)*'")
return makeVoid()
# ---------------------------------------------------------------------------
# Function body checking
# ---------------------------------------------------------------------------
@@ -2106,8 +2115,11 @@ proc checkFunc(sema: var Sema, decl: Decl) =
if hasTypeGeneric:
return
let wasChecked = sema.checkedFunc
let wasRelease = sema.releaseFunc
let wasAsync = sema.currentFuncIsAsync
sema.checkedFunc = "Checked" in decl.declAttrs
# C.4: @[Release] is the zero-cost escape — disables borrow checks even with @[Checked]
sema.releaseFunc = "Release" in decl.declAttrs
sema.checkedFunc = "Checked" in decl.declAttrs and not sema.releaseFunc
sema.currentFuncIsAsync = decl.declFuncIsAsync
if sema.checkedFunc:
sema.movedVars = @[]
@@ -2139,6 +2151,7 @@ proc checkFunc(sema: var Sema, decl: Decl) =
for tp in addedTypeParams:
sema.typeTable.del(tp)
sema.checkedFunc = wasChecked
sema.releaseFunc = wasRelease
sema.currentFuncIsAsync = wasAsync
sema.varRefLifetime = initTable[string, string]()
sema.returnLifetime = ""
+5
View File
@@ -64,6 +64,8 @@ type
tkDyn # dyn
tkDefer # defer
tkLifetime # 'a (lifetime parameter)
tkMacro # macro (declarative macro! definitions)
tkDollar # bare $ for macro rep $( ... )*
##Punctuation
tkLParen # (
@@ -224,6 +226,7 @@ proc keywordKind*(text: string): TokenKind =
of "comptime": tkComptime
of "dyn": tkDyn
of "defer": tkDefer
of "macro": tkMacro
of "true", "false": tkBoolLiteral
else: tkIdent
@@ -281,6 +284,8 @@ proc tokenKindName*(kind: TokenKind): string =
of tkComptime: "'comptime'"
of tkDyn: "'dyn'"
of tkDefer: "'defer'"
of tkMacro: "'macro'"
of tkDollar: "'$'"
of tkLifetime: "lifetime"
of tkLParen: "'('"
of tkRParen: "')'"