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
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:
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = ""
|
||||
|
||||
@@ -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: "')'"
|
||||
|
||||
Reference in New Issue
Block a user