feat: field-move Drop (partial/nested/ptr), stmt/pat macros, Windows runtime
ci / build (ubuntu) (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
selfhost-loop / bootstrap determinism (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 / CI gate (push) Has been cancelled

Sessions 70–74: partialMovedPaths + ptrAliases in bootstrap/selfhost CBE,
remaining drops after field moves, macro stmt/pat fragments, runtime_win.c
and MinGW hello CI, examples + drop-move smoke coverage, QUALITY_PLAN update.
This commit is contained in:
2026-07-21 12:29:53 +03:00
parent fe3b1e8b6a
commit a939f74b1b
22 changed files with 2672 additions and 117 deletions
+10
View File
@@ -134,6 +134,8 @@ type
ekStringInterp
ekClosure
ekMacroCall ## name!(args) — expanded before sema
ekMacroStmt ## `$s:stmt` arg wrapper (expand only)
ekMacroPat ## `$p:pat` arg wrapper (expand only)
MatchArm* = object
loc*: SourceLocation
@@ -243,6 +245,12 @@ type
## Group lengths for multi-rep: `m!(1,2; 3,4)` → @[2, 2].
## Empty means a single group of all args.
exprMacroGroupLens*: seq[int]
of ekMacroStmt:
## Statement fragment argument (`$s:stmt`) — only during expand
exprMacroStmt*: Stmt
of ekMacroPat:
## Pattern fragment argument (`$p:pat`) — only during expand
exprMacroPat*: Pattern
# ---------------------------------------------------------------------------
# Statements
@@ -375,6 +383,8 @@ type
mfkTt ## token-tree (MVP: same as expr)
mfkLiteral ## int/float/string/char/bool literal only
mfkBlock ## block expression `{ … }`
mfkStmt ## one statement (let/if/… or expression-stmt)
mfkPat ## match/let pattern
MacroFragment* = object
name*: string ## primary / first name (compat)
+34 -7
View File
@@ -805,11 +805,16 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
return 1
let baseDir = stdlibDir.parentDir()
let runtimeSrc = baseDir / "rt" / "runtime.c"
# Windows / BUX_RUNTIME=win → minimal runtime (no pthread/OpenSSL).
# Full POSIX runtime is rt/runtime.c.
let forceWinRt = getEnv("BUX_RUNTIME") == "win" or getEnv("BUX_RUNTIME") == "windows"
let useWinRt = forceWinRt or (when defined(windows): true else: false)
let runtimeName = if useWinRt: "runtime_win.c" else: "runtime.c"
let runtimeSrc = baseDir / "rt" / runtimeName
if fileExists(runtimeSrc):
copyFile(runtimeSrc, runtimeDst)
else:
printError("runtime.c not found in rt/", useColor)
printError(&"{runtimeName} not found in rt/", useColor)
return 1
let ioSrc = baseDir / "rt" / "io.c"
@@ -821,13 +826,31 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
# Compile with cc — debug default (-O0 -g) or --release (-O2)
let outputName = if pctx.man.name != "": pctx.man.name else: "bux_out"
let outputFile = buildDir / outputName
let exeSuffix = when defined(windows): ".exe" else: ""
let outputFile = buildDir / (outputName & exeSuffix)
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
# --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"
# Host C toolchain + link flags
let envCc = getEnv("BUX_CC")
let ccBin =
if envCc.len > 0: envCc
else:
when defined(windows): "gcc"
else: "cc"
let ldStable =
when defined(linux):
if useWinRt: "" else: " -Wl,--build-id=none"
else:
""
# Note: -l libs must come *after* .c/.o inputs (GNU ld left-to-right).
let (hostCflags, hostLibs) =
if useWinRt:
# gc-sections drops mono stdlib that is never called (crypto/tasks, …)
(" -ffunction-sections -fdata-sections", " -Wl,--gc-sections -lm")
else:
(" -pthread" & ldStable, " -lm -lcrypto")
let ccCmd = &"{ccBin} {cflags}{hostCflags} -o {outputFile} {cFile} {runtimeDst} {ioDst}{hostLibs} 2>&1"
if opts.verbose:
printInfo(&"running: {ccCmd}", useColor)
let (output, exitCode) = execCmdEx(ccCmd)
@@ -848,7 +871,11 @@ proc cmdRun*(args: seq[string], opts: GlobalOptions): int =
return buildRes
let man = loadManifest(root / "bux.toml")
let outputName = if man.name != "": man.name else: "bux_out"
let outputFile = root / "build" / outputName
let exeSuffix = when defined(windows): ".exe" else: ""
var outputFile = root / "build" / (outputName & exeSuffix)
if not fileExists(outputFile):
# Fallback without suffix (cross-env / older builds)
outputFile = root / "build" / outputName
if not fileExists(outputFile):
printError("executable not found after build", useColor)
return 1
+223 -16
View File
@@ -38,6 +38,14 @@ type
## Locals whose value was moved into another owner (struct field, let, return).
## Auto-Drop is skipped for these (session 37 — field-move ownership).
movedOutLocals*: HashSet[string]
## Partial field moves: local → dotted paths moved out by value
## (e.g. "items", "inner.items" for nested `a.b.c` — session 70/73).
## When parent Type_Drop is skipped, remaining droppable fields still Drop.
## Whole-local moves leave this empty → full skip, no field drops.
partialMovedFields*: Table[string, HashSet[string]]
## Pointer aliases: local pointer name → pointee local (`p = &bag` → p→bag).
## Used so `p.items` / `(*p).items` mark the owner local (session 74).
ptrAliases*: Table[string, string]
proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter
@@ -85,23 +93,80 @@ 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)
# Forward decls (used by markMovedOutFromAst / remainingFieldDrops before defs)
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]): Type
proc resolvePtrAlias(ctx: LowerCtx, name: string): string =
## Follow `p → bag` aliases (depth-limited).
result = name
var guard = 0
while result.len > 0 and ctx.ptrAliases.hasKey(result) and guard < 8:
result = ctx.ptrAliases[result]
inc guard
proc fieldPathFromAst(ctx: LowerCtx, expr: Expr): tuple[base: string, path: seq[string]] =
## Walk `a.b.c` / `(*p).b.c` / `p.b` (auto-deref) → owner local + path.
## Resolves pointer aliases (`p = &bag` → owner is `bag`).
result = ("", @[])
if expr == nil: return
var path: seq[string] = @[]
var e = expr
while e != nil and e.kind == ekField:
path.insert(e.exprFieldName, 0)
e = e.exprFieldObj
# Peel explicit derefs: (*p).x or (**pp).x
while e != nil and e.kind == ekUnary and e.exprUnaryOp == tkStar:
e = e.exprUnaryOperand
if e != nil and e.kind == ekIdent and e.exprIdent.len > 0 and path.len > 0:
let owner = ctx.resolvePtrAlias(e.exprIdent)
result = (owner, path)
proc pathKey(path: seq[string]): string =
path.join(".")
proc recordPtrAliasFromAst(ctx: var LowerCtx, ptrName: string, init: Expr) =
## If `init` is `&local` (possibly with paren/cast noise), record ptr→local.
if ptrName.len == 0 or init == nil: return
var e = init
# Skip simple casts
while e != nil and e.kind == ekCast:
e = e.exprCastOperand
if e != nil and e.kind == ekUnary and e.exprUnaryOp == tkAmp:
var op = e.exprUnaryOperand
while op != nil and op.kind == ekCast:
op = op.exprCastOperand
if op != nil and op.kind == ekIdent and op.exprIdent.len > 0:
ctx.ptrAliases[ptrName] = op.exprIdent
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).
## Partial field moves: `return bag.items` / `return outer.inner.items` /
## `return p.items` (p = &bag) mark the **owner** local so auto-Drop of the
## parent is skipped. Records dotted path so remaining fields still Drop
## (sessions 70/73/74).
if expr == nil: return
case expr.kind
of ekIdent:
ctx.markMovedOutLocal(expr.exprIdent)
ctx.markMovedOutLocal(ctx.resolvePtrAlias(expr.exprIdent))
of ekField:
let fieldTy = ctx.resolveExprType(expr)
if ctx.autoDropFuncName(fieldTy).len > 0:
ctx.markMovedOutFromAst(expr.exprFieldObj)
let (base, path) = ctx.fieldPathFromAst(expr)
if base.len > 0 and path.len > 0:
if not ctx.partialMovedFields.hasKey(base):
ctx.partialMovedFields[base] = initHashSet[string]()
ctx.partialMovedFields[base].incl(pathKey(path))
ctx.markMovedOutLocal(base)
# Nested path recorded as a whole — do not recurse (would mis-mark intermediates)
of ekUnary:
# Moving `*p` by value (whole pointee) — mark owner local if known
if expr.exprUnaryOp == tkStar and expr.exprUnaryOperand != nil and
expr.exprUnaryOperand.kind == ekIdent:
let owner = ctx.resolvePtrAlias(expr.exprUnaryOperand.exprIdent)
ctx.markMovedOutLocal(owner)
of ekStructInit:
for f in expr.exprStructInitFields:
ctx.markMovedOutFromAst(f.value)
@@ -119,6 +184,136 @@ proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool =
if target in ctx.movedOutLocals: return true
false
proc structFieldsOf(ctx: var LowerCtx, te: TypeExpr, typeName: string): seq[tuple[name: string, typ: Type]] =
## Resolve struct fields for a named / monomorphized type.
result = @[]
var declName = if te != nil: te.typeName else: ""
if declName.len == 0: declName = typeName
let sym = ctx.globalScope.lookup(declName)
if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct:
for f in sym.decl.declStructFields:
if f.ftype == nil: continue
var fieldTy: Type
if te != nil and te.typeArgs.len > 0 and ctx.genericStructs.hasKey(declName):
var subst = initTable[string, Type]()
let gdecl = ctx.genericStructs[declName]
for j, tp in gdecl.declStructTypeParams:
if j < te.typeArgs.len:
subst[tp.name] = ctx.resolveTypeExpr(te.typeArgs[j])
fieldTy = substituteType(ctx, f.ftype, subst)
else:
fieldTy = ctx.resolveTypeExpr(f.ftype)
result.add((f.name, fieldTy))
return
if ctx.structInstMap.hasKey(typeName):
for es in ctx.extraStructs:
if es.name == typeName:
for f in es.fields:
result.add((f.name, f.typ))
return
# Also try mangled typeName as decl name
let sym2 = ctx.globalScope.lookup(typeName)
if sym2 != nil and sym2.decl != nil and sym2.decl.kind == dkStruct:
for f in sym2.decl.declStructFields:
if f.ftype == nil: continue
result.add((f.name, ctx.resolveTypeExpr(f.ftype)))
proc makeFieldPtrAt(ctx: var LowerCtx, base: HirNode, rootTe: TypeExpr,
rootTypeName: string, path: seq[string], fieldTy: Type,
loc: SourceLocation): HirNode =
## `&(base.a.b)` with typed intermediate field accesses (needed by LIR/C).
if path.len == 0:
return hirUnary(tkAmp, base, makePointer(fieldTy), loc)
if path.len == 1:
return HirNode(kind: hFieldPtr, fieldPtrBase: base, fieldName: path[0],
typ: makePointer(fieldTy), loc: loc)
# Build typed prefix: base.a.b for path [a,b,c] → access a, then b; ptr on c
var cur = base
var curTe = rootTe
var curTypeName = rootTypeName
for i in 0 ..< path.len - 1:
let fields = ctx.structFieldsOf(curTe, curTypeName)
var nextTy: Type = makeUnknown()
for f in fields:
if f.name == path[i]:
nextTy = f.typ
break
cur = HirNode(kind: hFieldAccess, fieldAccessBase: cur,
fieldAccessName: path[i], typ: nextTy, loc: loc)
if nextTy != nil and nextTy.kind == tkNamed:
curTypeName = nextTy.name
curTe = TypeExpr(kind: tekNamed, typeName: nextTy.name)
else:
curTe = nil
curTypeName = ""
return HirNode(kind: hFieldPtr, fieldPtrBase: cur, fieldName: path[^1],
typ: makePointer(fieldTy), loc: loc)
proc remainingDropsAt(ctx: var LowerCtx, baseHir: HirNode, typeName: string,
te: TypeExpr, prefix: seq[string],
moved: HashSet[string], loc: SourceLocation,
rootTe: TypeExpr, rootTypeName: string): seq[HirNode] =
## Emit Drops for fields of `typeName` under `baseHir`+`prefix`, respecting
## dotted moved paths (exact = fully moved; prefix = recurse nested).
## `rootTe`/`rootTypeName` are the original local's type (for path typing).
result = @[]
let fields = ctx.structFieldsOf(te, typeName)
for f in fields:
var fpath = prefix
fpath.add(f.name)
let key = pathKey(fpath)
# Fully moved this field
if key in moved:
continue
# Nested partial: some path starts with key + "."
var nestedMoved = false
for m in moved:
if m.startsWith(key & "."):
nestedMoved = true
break
if nestedMoved:
let fty = f.typ
if fty == nil or fty.kind != tkNamed: continue
var fte = TypeExpr(kind: tekNamed, typeName: fty.name)
result.add(ctx.remainingDropsAt(baseHir, fty.name, fte, fpath, moved, loc,
rootTe, rootTypeName))
continue
# Unrelated field — full Drop if droppable
let dropFn = ctx.autoDropFuncName(f.typ)
if dropFn.len == 0: continue
let fieldPtr = ctx.makeFieldPtrAt(baseHir, rootTe, rootTypeName, fpath, f.typ, loc)
result.add(hirCall(dropFn, @[fieldPtr], makeVoid(), loc))
proc remainingFieldDrops(ctx: var LowerCtx, localName: string, loc: SourceLocation): seq[HirNode] =
## After a partial field move out of `localName`, Drop every *other* droppable
## field (including nested remaining after `a.b.c` moves).
result = @[]
if localName.len == 0 or not ctx.partialMovedFields.hasKey(localName):
return
let moved = ctx.partialMovedFields[localName]
if not ctx.varTypeExprs.hasKey(localName):
return
let te = ctx.varTypeExprs[localName]
if te == nil or te.kind != tekNamed:
return
let localTy = ctx.resolveTypeExpr(te)
if localTy == nil or localTy.kind != tkNamed:
return
let base = hirVar(localName, localTy, loc)
result = ctx.remainingDropsAt(base, localTy.name, te, @[], moved, loc, te, localTy.name)
proc emitDropOrPartial(ctx: var LowerCtx, stmts: var seq[HirNode], dropNode: HirNode,
skipName: string) =
## Emit Type_Drop, or remaining field Drops after a partial move.
if not ctx.shouldSkipDrop(dropNode, skipName):
stmts.add(dropNode)
return
let target = dropTargetName(dropNode)
if target.len > 0 and target in ctx.partialMovedFields:
let loc = if dropNode != nil: dropNode.loc else: SourceLocation()
for d in ctx.remainingFieldDrops(target, loc):
stmts.add(d)
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string =
## Return `Type_Drop` if this type should be auto-dropped, else "".
## Also monomorphizes generic Drop/Free helpers for stdlib collections.
@@ -195,8 +390,6 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode =
return nil
return hirLit(pat.patLit, litTokenType(pat.patLit), loc)
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
subjectEnumName: string, subjectHasData: bool,
loc: SourceLocation): HirNode =
@@ -527,6 +720,8 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.patternBoundNames = initHashSet[string]()
result.patternRenames = initTable[string, string]()
result.movedOutLocals = initHashSet[string]()
result.partialMovedFields = initTable[string, HashSet[string]]()
result.ptrAliases = initTable[string, string]()
proc sanitizeFatPart(s: string): string =
result = s.replace("const char*", "cstr").replace("unsigned int", "uint")
@@ -1466,6 +1661,9 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
return HirNode(kind: hAssign, assignOp: tkAssign,
assignTarget: loadTarget, assignValue: value,
typ: makeVoid(), loc: loc)
# Pointer alias update: `p = &bag`
if expr.exprAssignTarget.kind == ekIdent and expr.exprAssignValue != nil:
ctx.recordPtrAliasFromAst(expr.exprAssignTarget.exprIdent, expr.exprAssignValue)
let target = ctx.lowerExpr(expr.exprAssignTarget)
let value = ctx.lowerExpr(expr.exprAssignValue)
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
@@ -1842,6 +2040,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if initHir != nil:
let store = hirStore(varNode, initHir, loc)
stmts.add(store)
# Pointer alias: `let p = &bag` so later `p.items` marks bag (session 74)
if stmt.stmtLetInit != nil:
ctx.recordPtrAliasFromAst(stmt.stmtLetName, stmt.stmtLetInit)
# Move: `let a = b` takes ownership of droppable local `b`
if stmt.stmtLetInit != nil:
ctx.markMovedOutFromAst(stmt.stmtLetInit)
@@ -1879,8 +2080,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
retVal = hirVar(tmp, retTy, loc)
# Add defers in reverse order (LIFO); snapshot full stack for every return path
for i in countdown(ctx.deferStmts.len - 1, 0):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i])
ctx.emitDropOrPartial(stmts, ctx.deferStmts[i], skipDrop)
stmts.add(hirReturn(retVal, loc))
return hirBlock(stmts, nil, makeVoid(), loc)
@@ -2201,8 +2401,7 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
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.emitDropOrPartial(stmts, ctx.deferStmts[i], skipDrop)
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
@@ -2258,8 +2457,12 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
ctx.patternRenames = initTable[string, string]()
let oldDefers = ctx.deferStmts
let oldMovedOut = ctx.movedOutLocals
let oldPartialMoved = ctx.partialMovedFields
let oldPtrAliases = ctx.ptrAliases
ctx.deferStmts = @[]
ctx.movedOutLocals = initHashSet[string]()
ctx.partialMovedFields = initTable[string, HashSet[string]]()
ctx.ptrAliases = initTable[string, string]()
# Add parameters to varTypeExprs after clearing so they are visible in the body.
for p in funcParams:
if p.ptype != nil:
@@ -2279,10 +2482,14 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
hasReturn = true
if not hasReturn:
for i in countdown(ctx.deferStmts.len - 1, 0):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], ""):
body.blockStmts.add(ctx.deferStmts[i])
ctx.deferStmts = oldDefers
ctx.movedOutLocals = oldMovedOut
ctx.emitDropOrPartial(body.blockStmts, ctx.deferStmts[i], "")
# Always restore — mono of generics (generateMethodInstance → lowerFunc) nests
# inside an outer function. Restoring only when deferStmts.len > 0 wiped the
# caller's Drop stack (PeekTagAndTake lost Array_Drop after Array_Len mono).
ctx.deferStmts = oldDefers
ctx.movedOutLocals = oldMovedOut
ctx.partialMovedFields = oldPartialMoved
ctx.ptrAliases = oldPtrAliases
ctx.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType
+153 -16
View File
@@ -3,7 +3,7 @@
## Hygiene: substitute clones args at call-site, graft call-site SourceLocation
## onto expanded template nodes (Ast_QuoteCallSite policy from QUALITY_PLAN).
import std/[tables, sequtils, sets]
import std/[tables, sequtils, sets, strutils]
import ast, token, source_location
type
@@ -193,9 +193,16 @@ proc cloneExpr*(e: Expr): Expr =
captureCount: 0, captureNames: @[], captureTypeKinds: @[])
of ekMacroCall:
result = Expr(kind: ekMacroCall, loc: e.loc,
exprMacroName: e.exprMacroName, exprMacroArgs: @[])
exprMacroName: e.exprMacroName, exprMacroArgs: @[],
exprMacroGroupLens: e.exprMacroGroupLens)
for a in e.exprMacroArgs:
result.exprMacroArgs.add(cloneExpr(a))
of ekMacroStmt:
result = Expr(kind: ekMacroStmt, loc: e.loc,
exprMacroStmt: cloneStmt(e.exprMacroStmt))
of ekMacroPat:
result = Expr(kind: ekMacroPat, loc: e.loc,
exprMacroPat: clonePattern(e.exprMacroPat))
proc cloneStmt*(s: Stmt): Stmt =
if s == nil: return nil
@@ -329,6 +336,10 @@ proc graftExprLoc(e: Expr, loc: SourceLocation) =
of ekClosure: graftBlockLoc(e.exprClosureBody, loc)
of ekMacroCall:
for a in e.exprMacroArgs: graftExprLoc(a, loc)
of ekMacroStmt:
graftStmtLoc(e.exprMacroStmt, loc)
of ekMacroPat:
discard
else: discard
proc graftStmtLoc(s: Stmt, loc: SourceLocation) =
@@ -530,6 +541,46 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr
proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt
proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block
proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): seq[Stmt]
proc substPattern(p: Pattern, env: MacroEnv, callLoc: SourceLocation): Pattern
proc substPattern(p: Pattern, env: MacroEnv, callLoc: SourceLocation): Pattern =
## Substitute `$p:pat` (pkIdent `$name`) with the bound pattern.
if p == nil: return nil
if p.kind == pkIdent and env.singles.hasKey(p.patIdent):
let bound = env.singles[p.patIdent]
if bound != nil and bound.kind == ekMacroPat:
result = clonePattern(bound.exprMacroPat)
if result != nil: result.loc = callLoc
return
case p.kind
of pkRange:
result = Pattern(kind: pkRange, loc: callLoc,
patRangeLo: substPattern(p.patRangeLo, env, callLoc),
patRangeHi: substPattern(p.patRangeHi, env, callLoc),
patRangeInclusive: p.patRangeInclusive)
of pkEnum:
result = Pattern(kind: pkEnum, loc: callLoc, patEnumPath: p.patEnumPath,
patEnumArgs: @[], patEnumNamed: @[])
for a in p.patEnumArgs:
result.patEnumArgs.add(substPattern(a, env, callLoc))
for nf in p.patEnumNamed:
result.patEnumNamed.add((nf.name, substPattern(nf.pattern, env, callLoc)))
of pkStruct:
result = Pattern(kind: pkStruct, loc: callLoc, patStructName: p.patStructName,
patStructFields: @[])
for f in p.patStructFields:
result.patStructFields.add((f.name, substPattern(f.pattern, env, callLoc)))
of pkTuple:
result = Pattern(kind: pkTuple, loc: callLoc, patTupleElements: @[])
for el in p.patTupleElements:
result.patTupleElements.add(substPattern(el, env, callLoc))
of pkGuarded:
result = Pattern(kind: pkGuarded, loc: callLoc,
patGuardedInner: substPattern(p.patGuardedInner, env, callLoc),
patGuardedExpr: substExpr(p.patGuardedExpr, env, callLoc))
else:
result = clonePattern(p)
if result != nil: result.loc = callLoc
proc substBlock(b: Block, env: MacroEnv, callLoc: SourceLocation): Block =
if b == nil: return nil
@@ -611,6 +662,14 @@ proc substStmtsFlat(stmts: seq[Stmt], env: MacroEnv, callLoc: SourceLocation): s
if body != nil:
for st in body.stmts:
result.add(st)
elif s.kind == skExpr and s.stmtExpr != nil and s.stmtExpr.kind == ekIdent and
env.singles.hasKey(s.stmtExpr.exprIdent):
let bound = env.singles[s.stmtExpr.exprIdent]
if bound != nil and bound.kind == ekMacroStmt:
# Splice `$s:stmt` as a real statement (not an expression)
result.add(substStmt(bound.exprMacroStmt, env, callLoc))
else:
result.add(substStmt(s, env, callLoc))
else:
result.add(substStmt(s, env, callLoc))
@@ -659,7 +718,7 @@ proc substStmt(s: Stmt, env: MacroEnv, callLoc: SourceLocation): Stmt =
c.stmtMatchSubject = substExpr(c.stmtMatchSubject, env, callLoc)
var arms: seq[MatchArm] = @[]
for arm in c.stmtMatchArms:
arms.add(MatchArm(loc: callLoc, pattern: arm.pattern,
arms.add(MatchArm(loc: callLoc, pattern: substPattern(arm.pattern, env, callLoc),
body: substExpr(arm.body, env, callLoc)))
c.stmtMatchArms = arms
of skReturn:
@@ -775,7 +834,7 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
c.exprMatchSubject = substExpr(c.exprMatchSubject, env, callLoc)
var arms: seq[MatchArm] = @[]
for arm in c.exprMatchArms:
arms.add(MatchArm(loc: callLoc, pattern: arm.pattern,
arms.add(MatchArm(loc: callLoc, pattern: substPattern(arm.pattern, env, callLoc),
body: substExpr(arm.body, env, callLoc)))
c.exprMatchArms = arms
of ekStringInterp:
@@ -871,14 +930,89 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
if f.kinds.len > 0: return f.kinds
@[f.kind]
proc exprToPattern(arg: Expr): Pattern =
## Convert a call-site expression into a pattern for `$p:pat`.
if arg == nil: return nil
if arg.kind == ekMacroPat: return clonePattern(arg.exprMacroPat)
case arg.kind
of ekIdent:
if arg.exprIdent == "_":
return Pattern(kind: pkWildcard, loc: arg.loc)
return Pattern(kind: pkIdent, loc: arg.loc, patIdent: arg.exprIdent)
of ekLiteral:
return Pattern(kind: pkLiteral, loc: arg.loc, patLit: arg.exprLit)
of ekPath:
return Pattern(kind: pkEnum, loc: arg.loc, patEnumPath: arg.exprPath,
patEnumArgs: @[], patEnumNamed: @[])
of ekCall:
# Enum::Variant(args) or Variant(args)
var path: seq[string] = @[]
if arg.exprCallCallee == nil: return nil
case arg.exprCallCallee.kind
of ekIdent: path = @[arg.exprCallCallee.exprIdent]
of ekPath: path = arg.exprCallCallee.exprPath
else: return nil
var pargs: seq[Pattern] = @[]
for a in arg.exprCallArgs:
let ap = exprToPattern(a)
if ap == nil: return nil
pargs.add(ap)
return Pattern(kind: pkEnum, loc: arg.loc, patEnumPath: path,
patEnumArgs: pargs, patEnumNamed: @[])
of ekTuple:
var elems: seq[Pattern] = @[]
for el in arg.exprTupleElements:
let ep = exprToPattern(el)
if ep == nil: return nil
elems.add(ep)
return Pattern(kind: pkTuple, loc: arg.loc, patTupleElements: elems)
of ekStructInit:
var fields: seq[tuple[name: string, pattern: Pattern]] = @[]
for f in arg.exprStructInitFields:
let fp = exprToPattern(f.value)
if fp == nil: return nil
fields.add((f.name, fp))
return Pattern(kind: pkStruct, loc: arg.loc,
patStructName: arg.exprStructInitName, patStructFields: fields)
of ekRange:
let lo = exprToPattern(arg.exprRangeLo)
let hi = exprToPattern(arg.exprRangeHi)
if lo == nil or hi == nil: return nil
return Pattern(kind: pkRange, loc: arg.loc, patRangeLo: lo, patRangeHi: hi,
patRangeInclusive: arg.exprRangeInclusive)
else:
return nil
proc coerceArg(k: MacroFragKind, arg: Expr): Expr =
## Normalize arg for storage (pat → ekMacroPat). Returns nil if kind fails.
if arg == nil: return nil
case k
of mfkIdent:
if arg.kind != ekIdent: return nil
return arg
of mfkLiteral:
if arg.kind != ekLiteral: return nil
return arg
of mfkBlock:
if arg.kind != ekBlock: return nil
return arg
of mfkStmt:
if arg.kind == ekMacroStmt: return arg
# Expression as expression-statement
if arg.kind in {ekMacroPat}: return nil
return Expr(kind: ekMacroStmt, loc: arg.loc,
exprMacroStmt: Stmt(kind: skExpr, loc: arg.loc, stmtExpr: arg))
of mfkPat:
let pat = exprToPattern(arg)
if pat == nil: return nil
return Expr(kind: ekMacroPat, loc: arg.loc, exprMacroPat: pat)
of mfkExpr, mfkTt:
if arg.kind in {ekMacroStmt, ekMacroPat}: return nil
return arg
proc fragMatches(k: MacroFragKind, arg: Expr): bool =
## Kind constraint at match time (after arg expand).
if arg == nil: return false
case k
of mfkIdent: arg.kind == ekIdent
of mfkLiteral: arg.kind == ekLiteral
of mfkBlock: arg.kind == ekBlock
of mfkExpr, mfkTt: true
coerceArg(k, arg) != nil
var matched: MacroRule
var env: MacroEnv
@@ -916,10 +1050,11 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
for c in 0 ..< chunk:
let arg = g[i + c]
let k = if c < ks.len: ks[c] else: mfkExpr
if not fragMatches(k, arg):
let coerced = coerceArg(k, arg)
if coerced == nil:
failed = true
break
e.lists[ns[c]].add(arg)
e.lists[ns[c]].add(coerced)
if failed: break
i += chunk
else:
@@ -930,10 +1065,11 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
for c in 0 ..< chunk:
let arg = flat[ai]
let k = if c < ks.len: ks[c] else: mfkExpr
if not fragMatches(k, arg):
let coerced = coerceArg(k, arg)
if coerced == nil:
failed = true
break
e.lists[ns[c]].add(arg)
e.lists[ns[c]].add(coerced)
inc ai
if failed: break
else:
@@ -954,11 +1090,12 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
arg = flat[ai]
inc ai
let k = if ks.len > 0: ks[0] else: frag.kind
if not fragMatches(k, arg):
let coerced = coerceArg(k, arg)
if coerced == nil:
failed = true
break
let n = if ns.len > 0: ns[0] else: frag.name
e.singles[n] = arg
e.singles[n] = coerced
if not failed:
if useGroups:
+25 -3
View File
@@ -429,6 +429,26 @@ proc parseAssign(p: var Parser): Expr
proc parseExpr(p: var Parser): Expr =
p.parseAssign()
proc isMacroStmtStart(p: Parser): bool =
## Keywords that begin a statement (for `$s:stmt` call-site args).
p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn,
tkBreak, tkContinue, tkDefer, tkSwitch, tkDo}
proc parseMacroArg(p: var Parser): Expr =
## Macro call argument:
## - statement keywords → ekMacroStmt
## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce)
## - else expression
let loc = p.currentLoc
if p.isMacroStmtStart():
let st = p.parseStmt()
return Expr(kind: ekMacroStmt, loc: loc, exprMacroStmt: st)
# Wildcard is not a valid expression; parse as pattern for `$p:pat`
if p.check(tkUnderscore):
let pat = p.parsePattern()
return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat)
p.parseExpr()
proc parseStringInterpolation(p: var Parser, tok: Token): Expr =
## Parse a string literal that contains {expr} interpolations.
let text = tok.text
@@ -749,7 +769,7 @@ proc parsePostfix(p: var Parser): Expr =
curGroup = 0
p.skipNewlines()
continue
margs.add(p.parseExpr())
margs.add(p.parseMacroArg())
inc curGroup
p.skipNewlines()
if p.check(tkComma):
@@ -1628,10 +1648,12 @@ proc parseMacroFragKind(p: var Parser, kindTok: Token): MacroFragKind =
of "tt": mfkTt
of "literal", "lit": mfkLiteral
of "block": mfkBlock
of "stmt": mfkStmt
of "pat", "pattern": mfkPat
else:
p.emitError(kindTok.loc,
"unsupported macro fragment kind '" & kindTok.text &
"' (expr|ident|tt|literal|block)")
"' (expr|ident|tt|literal|block|stmt|pat)")
mfkExpr
proc parseMacroFragment(p: var Parser): MacroFragment =
@@ -1640,7 +1662,7 @@ proc parseMacroFragment(p: var Parser): MacroFragment =
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 kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block|stmt|pat)")
let k = p.parseMacroFragKind(kindTok)
result = MacroFragment(
name: fragTok.text,
+4
View File
@@ -1902,6 +1902,10 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
# Should have been expanded before analyze; leftover is a compiler bug
sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'")
return makeUnknown()
of ekMacroStmt, ekMacroPat:
# Expand-only wrappers; must not reach type-checking
sema.emitError(expr.loc, "internal: unexpanded macro stmt/pat fragment")
return makeUnknown()
of ekClosure:
let savedRetType = sema.currentRetType
let savedClosureDepth = sema.closureDepth