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

Sessions 56–69: declarative macro! with rep/zip/literal/block and
unhygienic var $name binders; partial field-move skip Drop; @[Release]
polish; LSP type hierarchy; CI Nim cache + lean macOS + Windows smoke.
This commit is contained in:
2026-07-20 17:19:46 +03:00
parent 6f2a3b1d88
commit fe3b1e8b6a
41 changed files with 5281 additions and 141 deletions
+32 -1
View File
@@ -85,12 +85,23 @@ proc markMovedOutLocal(ctx: var LowerCtx, name: string) =
if name.len > 0 and ctx.hasPendingDrop(name):
ctx.movedOutLocals.incl(name)
# Forward decls (used by markMovedOutFromAst before their full definitions)
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string
proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
## Mark droppable locals used by-value in ownership-taking contexts.
## Partial field moves: `return bag.items` / `let x = bag.items` mark `bag`
## so auto-Drop of the parent is skipped — **only when the field type itself
## is droppable** (not `return bag.tag` for an int field).
if expr == nil: return
case expr.kind
of ekIdent:
ctx.markMovedOutLocal(expr.exprIdent)
of ekField:
let fieldTy = ctx.resolveExprType(expr)
if ctx.autoDropFuncName(fieldTy).len > 0:
ctx.markMovedOutFromAst(expr.exprFieldObj)
of ekStructInit:
for f in expr.exprStructInitFields:
ctx.markMovedOutFromAst(f.value)
@@ -2131,6 +2142,11 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
of skMacroRep:
# Expanded before lowering
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
## asExpr=true: block is used as a value (`let x = { ... }`, match arm body).
## Last skExpr becomes the block result. Statement blocks (func body, if/while)
@@ -2168,15 +2184,30 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
expr = last.blockExpr
# Scope exit: Drop locals introduced in this block (not outer ones).
# Skip Drop for block result and any moved-out locals (field / let / return move).
# If the last statement always returns, drops were already injected on that
# path — re-emitting them here produces dead double-Drop after `return`.
proc blockAlwaysReturns(n: HirNode): bool =
if n == nil: return false
if n.kind == hReturn: return true
if n.kind == hBlock:
if n.blockStmts.len == 0: return false
return blockAlwaysReturns(n.blockStmts[^1])
false
var skipDrop = ""
if expr != nil and expr.kind == hVar:
skipDrop = expr.varName
ctx.markMovedOutLocal(expr.varName)
if ctx.deferStmts.len > deferBase:
let lastAlwaysReturns = stmts.len > 0 and blockAlwaysReturns(stmts[^1])
if ctx.deferStmts.len > deferBase and not lastAlwaysReturns:
for i in countdown(ctx.deferStmts.len - 1, deferBase):
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i])
ctx.deferStmts.setLen(deferBase)
elif ctx.deferStmts.len > deferBase and lastAlwaysReturns:
# Return path already owns these drops; pop so outer scopes don't re-run them
# for the same locals when this block is nested. Outer live locals remain.
ctx.deferStmts.setLen(deferBase)
let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid()
return hirBlock(stmts, expr, typ, blk.loc, isScope = true)