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
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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user