feat: match guards, HOF inference, ownership C.2/C.3, LSP sema hover

Sessions 18–23 quality work:

- B.3c match arm guards + sequential found-flag lower (bootstrap + selfhost)
- Generic HOF type inference (Array/Iter map/filter/fold without type args)
- Pattern binding shadowing via unique C locals (__pN_src)
- Ownership C.2 exclusive &mut data-flow + C.4 goldens; *p= store-through fix
- Ownership C.3 auto-drop on early return/branches: scoped defers, move-on-return,
  Drop monomorphization, materialize return before Drop
- LSP 0.3.0: hover from real sema types
- Examples and QUALITY_PLAN session log; selfhost-loop identical
This commit is contained in:
2026-07-18 21:52:14 +03:00
parent 66f11d1869
commit 3eb1ad3a82
25 changed files with 2246 additions and 457 deletions
+263 -99
View File
@@ -31,13 +31,83 @@ type
## All func types that need BuxFn_* typedefs (including locals)
seenFatTypes*: seq[Type]
## Pattern-binding names already alloca'd in the current function
## (avoids `int v;` twice when two matches bind the same name)
## (legacy; unique mangled names are preferred for shadowing safety)
patternBoundNames*: HashSet[string]
## Active renames: source pattern name → unique C local (for shadowing)
patternRenames*: Table[string, string]
proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter
result = "__tmp_" & $ctx.varCounter
proc freshPatName(ctx: var LowerCtx, src: string): string =
## Unique C name for a pattern binding (allows shadowing outer lets / nested matches).
inc ctx.varCounter
let safe = if src.len > 0 and src != "_": src else: "x"
result = "__p" & $ctx.varCounter & "_" & safe
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
proc namedTypeArg(name: string): TypeExpr =
TypeExpr(kind: tekNamed, typeName: name)
proc ensureDropMono(ctx: var LowerCtx, dropBase: string, freeBase: string, typeArgs: seq[TypeExpr]) =
## Monomorphize Free (if any) then Drop so the C linker finds them.
if freeBase.len > 0:
discard ctx.generateMethodInstance(freeBase, typeArgs)
discard ctx.generateMethodInstance(dropBase, typeArgs)
proc dropTargetsVar(n: HirNode, name: string): bool =
## True if n is Type_Drop(&name) / collection Drop of that local.
if n == nil or name.len == 0: return false
if n.kind == hCall and n.callArgs.len >= 1:
let a = n.callArgs[0]
if a != nil and a.kind == hUnary and a.unaryOp == tkAmp and
a.unaryOperand != nil and a.unaryOperand.kind == hVar:
return a.unaryOperand.varName == name
return false
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.
if ty == nil: return ""
var typeName = ""
if ty.kind == tkNamed:
typeName = ty.name
else:
return ""
# User type with @[Drop]
let sym = ctx.globalScope.lookup(typeName)
if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct:
if "Drop" in sym.decl.declAttrs:
return typeName & "_Drop"
# Explicit Type_Drop function exists (extend … for Drop)
let dropSym = ctx.globalScope.lookup(typeName & "_Drop")
if dropSym != nil and dropSym.kind == skFunc:
return typeName & "_Drop"
# Stdlib mangled collections: Array_int → Array_Drop_int
if typeName.startsWith("Array_"):
let elem = typeName[6 .. ^1]
ctx.ensureDropMono("Array_Drop", "Array_Free", @[namedTypeArg(elem)])
return "Array_Drop_" & elem
if typeName.startsWith("Map_"):
let rest = typeName[4 .. ^1]
let us = rest.find('_')
if us > 0:
let k = rest[0 ..< us]
let v = rest[us+1 .. ^1]
ctx.ensureDropMono("Map_Drop", "Map_Free", @[namedTypeArg(k), namedTypeArg(v)])
return "Map_Drop_" & rest
if typeName.startsWith("Set_"):
let elem = typeName[4 .. ^1]
ctx.ensureDropMono("Set_Drop", "Set_Free", @[namedTypeArg(elem)])
return "Set_Drop_" & elem
if typeName.startsWith("Channel_"):
let elem = typeName[8 .. ^1]
ctx.ensureDropMono("Channel_Drop", "Channel_Free", @[namedTypeArg(elem)])
return "Channel_Drop_" & elem
return ""
proc freshTryVar(ctx: var LowerCtx): string =
inc ctx.tryCounter
result = "__try_" & $ctx.tryCounter
@@ -73,9 +143,6 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode =
return nil
return hirLit(pat.patLit, litTokenType(pat.patLit), loc)
proc matchAlwaysTrue(loc: SourceLocation): HirNode =
hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc)
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
@@ -123,18 +190,33 @@ proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
# Single-segment enum path — always-true fallback
return nil
of pkGuarded:
# Guard: inner pattern AND guard expression (lowered later if needed)
# For now only support always-true inner + guard as bool expr via lowerExpr path.
# Guards are not yet fully lowered here (require expr lowering of guard).
return nil
# Condition is only the inner pattern; guard is applied after bindings in lowerMatch.
return matchPatternCond(ctx, subject, pattern.patGuardedInner, subjectEnumName, subjectHasData, loc)
else:
# Struct/tuple patterns: not yet fully lowered — always-true
return nil
# lowerMatch calls lowerExpr for arm bodies after emitting bindings
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
proc bindPatLocal(ctx: var LowerCtx, srcName: string, ty: Type, subject: HirNode,
loc: SourceLocation): seq[HirNode] =
## Allocate a unique C local for a pattern binding and map source name → C name.
result = @[]
if srcName.len == 0 or srcName == "_":
return
let cName = ctx.freshPatName(srcName)
ctx.patternRenames[srcName] = cName
ctx.patternBoundNames.incl(srcName)
result.add(hirAlloca(cName, ty, loc))
result.add(hirStore(hirVar(cName, ty, loc), subject, loc))
proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
subjectEnumName: string, subjectHasData: bool,
loc: SourceLocation): seq[HirNode] =
## Emit alloca+store for identifiers bound by a match pattern.
## Each binding gets a unique C name (`__pN_src`) so nested matches and
## outer `let` can share source names without C redeclaration / use-before-decl.
## Enum payload: `Option::Some(value)` → `value = subject.data.Some_0`
## Ident catch-all: `x` → `x = subject`
result = @[]
@@ -142,10 +224,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
case pattern.kind
of pkIdent:
let ty = if subject.typ != nil: subject.typ else: makeUnknown()
if pattern.patIdent notin ctx.patternBoundNames:
result.add(hirAlloca(pattern.patIdent, ty, loc))
ctx.patternBoundNames.incl(pattern.patIdent)
result.add(hirStore(hirVar(pattern.patIdent, ty, loc), subject, loc))
result.add(ctx.bindPatLocal(pattern.patIdent, ty, subject, loc))
of pkEnum:
if not subjectHasData:
return
@@ -195,10 +274,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if arg.kind == pkIdent:
if arg.patIdent notin ctx.patternBoundNames:
result.add(hirAlloca(arg.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(arg.patIdent)
result.add(hirStore(hirVar(arg.patIdent, fieldTy, loc), fieldLoad, loc))
result.add(ctx.bindPatLocal(arg.patIdent, fieldTy, fieldLoad, loc))
else:
# Nested: Option::Some((a, b)), Pair::Two(Point { x, y })
result.add(ctx.matchPatternBindings(fieldLoad, arg, subjectEnumName, subjectHasData, loc))
@@ -220,10 +296,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if nf.pattern.kind == pkIdent:
if nf.pattern.patIdent notin ctx.patternBoundNames:
result.add(hirAlloca(nf.pattern.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(nf.pattern.patIdent)
result.add(hirStore(hirVar(nf.pattern.patIdent, fieldTy, loc), fieldLoad, loc))
result.add(ctx.bindPatLocal(nf.pattern.patIdent, fieldTy, fieldLoad, loc))
else:
result.add(ctx.matchPatternBindings(fieldLoad, nf.pattern, subjectEnumName, subjectHasData, loc))
of pkGuarded:
@@ -241,10 +314,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if elem.kind == pkIdent:
if elem.patIdent notin ctx.patternBoundNames:
result.add(hirAlloca(elem.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(elem.patIdent)
result.add(hirStore(hirVar(elem.patIdent, fieldTy, loc), fieldLoad, loc))
result.add(ctx.bindPatLocal(elem.patIdent, fieldTy, fieldLoad, loc))
else:
# Nested patterns: recurse with field as subject
result.add(ctx.matchPatternBindings(fieldLoad, elem, subjectEnumName, subjectHasData, loc))
@@ -269,68 +339,114 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if fpat.kind == pkIdent:
if fpat.patIdent notin ctx.patternBoundNames:
result.add(hirAlloca(fpat.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(fpat.patIdent)
result.add(hirStore(hirVar(fpat.patIdent, fieldTy, loc), fieldLoad, loc))
result.add(ctx.bindPatLocal(fpat.patIdent, fieldTy, fieldLoad, loc))
else:
result.add(ctx.matchPatternBindings(fieldLoad, fpat, subjectEnumName, subjectHasData, loc))
else:
discard
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode =
## Lower match expression to a block with if-else chain.
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, astArms: seq[MatchArm], typ: Type, loc: SourceLocation): HirNode =
## Lower match expression to sequential ifs with a `found` flag.
## Supports: enum tags + payload bindings, integer/bool/char/string literals,
## ranges, wildcard/ident catch-all.
## ranges, wildcard/ident catch-all, and `p if guard` arms.
##
## Each arm:
## 1. emit unique pattern bindings (sets patternRenames)
## 2. lower guard + body (idents use renames)
## 3. restore renames
## if (!found) { if (cond) { binds; if (guard) { result=body; found=true } } }
let hasResult = typ != nil and typ.kind != tkVoid and typ.kind != tkUnknown
let resultName = ctx.freshName()
let foundName = ctx.freshName()
var stmts: seq[HirNode] = @[]
if hasResult:
stmts.add(hirAlloca(resultName, typ, loc))
stmts.add(hirAlloca(foundName, makeBool(), loc))
stmts.add(hirStore(hirVar(foundName, makeBool(), loc),
hirLit(Token(kind: tkBoolLiteral, text: "false", loc: loc), makeBool(), loc), loc))
# Determine whether the matched enum has data variants (needs .tag access).
var subjectEnumName = ""
var subjectHasData = false
if subject.typ != nil and subject.typ.kind == tkNamed:
subjectEnumName = subject.typ.name
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
proc makeArmBlock(body: HirNode, bindStmts: seq[HirNode]): HirNode =
var armStmts: seq[HirNode] = bindStmts
for arm in astArms:
# Snapshot renames so this arm's bindings don't leak to later arms
let savedRenames = ctx.patternRenames
var innerPat = arm.pattern
if arm.pattern != nil and arm.pattern.kind == pkGuarded:
innerPat = arm.pattern.patGuardedInner
# Register bind types for resolveExprType during body lower
if innerPat != nil and innerPat.kind == pkEnum and subjectHasData:
var enumName = ""
var variantName = ""
if innerPat.patEnumPath.len >= 2:
enumName = innerPat.patEnumPath[0]
variantName = innerPat.patEnumPath[^1]
elif innerPat.patEnumPath.len == 1:
variantName = innerPat.patEnumPath[0]
enumName = subjectEnumName
var fieldTypes: seq[Type] = @[]
let enumSym = ctx.globalScope.lookup(enumName)
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
for v in enumSym.decl.declEnumVariants:
if v.name == variantName:
for f in v.fields:
fieldTypes.add(ctx.resolveTypeExpr(f))
break
for i, arg in innerPat.patEnumArgs:
if arg != nil and arg.kind == pkIdent:
let ft = if i < fieldTypes.len: fieldTypes[i] else: makeInt()
ctx.varTypeExprs[arg.patIdent] = typeToTypeExpr(ft)
elif innerPat != nil and innerPat.kind == pkIdent:
let ty = if subject.typ != nil: subject.typ else: makeUnknown()
ctx.varTypeExprs[innerPat.patIdent] = typeToTypeExpr(ty)
# Bindings BEFORE body so (1) renames active (2) alloca precedes use in C
let binds = matchPatternBindings(ctx, subject, innerPat, subjectEnumName, subjectHasData, loc)
var guardHir: HirNode = nil
if arm.pattern != nil and arm.pattern.kind == pkGuarded and arm.pattern.patGuardedExpr != nil:
guardHir = ctx.lowerExpr(arm.pattern.patGuardedExpr)
let bodyHir = ctx.lowerExpr(arm.body)
# Pop this arm's renames (nested matches already restored themselves)
ctx.patternRenames = savedRenames
var successStmts: seq[HirNode] = @[]
if hasResult:
armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc))
elif body != nil:
# Void match: evaluate body for side effects
armStmts.add(body)
return hirBlock(armStmts, nil, makeVoid(), loc)
successStmts.add(hirStore(hirVar(resultName, typ, loc), bodyHir, loc))
elif bodyHir != nil:
successStmts.add(bodyHir)
successStmts.add(hirStore(hirVar(foundName, makeBool(), loc),
hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc), loc))
let successBlock = hirBlock(successStmts, nil, makeVoid(), loc)
# Build if-else chain from arms (last arm is the outermost else)
var ifChain: HirNode = nil
for i in countdown(arms.len - 1, 0):
let arm = arms[i]
let binds = matchPatternBindings(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc)
let armBlock = makeArmBlock(arm.body, binds)
let cond = matchPatternCond(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc)
if cond == nil:
# Always-true arm (wildcard / incomplete pattern)
if ifChain == nil:
ifChain = armBlock
else:
ifChain = HirNode(kind: hIf, ifCond: matchAlwaysTrue(loc), ifThen: armBlock,
ifElse: ifChain, typ: makeVoid(), loc: loc)
var afterBinds: HirNode
if guardHir != nil:
afterBinds = HirNode(kind: hIf, ifCond: guardHir, ifThen: successBlock, ifElse: nil,
typ: makeVoid(), loc: loc)
else:
if ifChain == nil:
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil,
typ: makeVoid(), loc: loc)
else:
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: ifChain,
typ: makeVoid(), loc: loc)
afterBinds = successBlock
if ifChain != nil:
stmts.add(ifChain)
var armInnerStmts = binds
armInnerStmts.add(afterBinds)
let armInner = hirBlock(armInnerStmts, nil, makeVoid(), loc)
let cond = matchPatternCond(ctx, subject, innerPat, subjectEnumName, subjectHasData, loc)
let armBody = if cond == nil: armInner
else: HirNode(kind: hIf, ifCond: cond, ifThen: armInner, ifElse: nil,
typ: makeVoid(), loc: loc)
let notFound = HirNode(kind: hUnary, unaryOp: tkBang,
unaryOperand: hirVar(foundName, makeBool(), loc),
typ: makeBool(), loc: loc)
stmts.add(HirNode(kind: hIf, ifCond: notFound, ifThen: armBody, ifElse: nil,
typ: makeVoid(), loc: loc))
if hasResult:
return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc)
@@ -357,6 +473,7 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.funcAdapterSigs = initTable[string, Type]()
result.seenFatTypes = @[]
result.patternBoundNames = initHashSet[string]()
result.patternRenames = initTable[string, string]()
proc sanitizeFatPart(s: string): string =
result = s.replace("const char*", "cstr").replace("unsigned int", "uint")
@@ -544,8 +661,7 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
return makeFunc(params, ret)
else: return makeUnknown()
# Forward declarations
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
# Forward declarations (lowerExpr already declared above for lowerMatch)
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc
@@ -854,8 +970,6 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
return typeToTypeExpr(concreteArgs[0])
return TypeExpr(kind: tekNamed, typeName: "unknown")
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
proc lowerExprWithDynRefCoerce(ctx: var LowerCtx, arg: Expr, expectedType: Type): HirNode =
## Lower an expression, coercing &Concrete to &dyn Trait if needed.
let lowered = ctx.lowerExpr(arg)
@@ -997,6 +1111,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekIdent:
let name = expr.exprIdent
# Pattern binding rename: source name → unique C local (`__pN_v`)
if ctx.patternRenames.hasKey(name):
let cName = ctx.patternRenames[name]
return hirVar(cName, typ, loc)
# Capture rewriting: if inside closure and ident is captured
if ctx.closureDepth > 0 and ctx.currentClosureExpr != nil and ctx.envInstanceName != "":
let idx = ctx.currentClosureExpr.captureNames.find(name)
@@ -1286,6 +1404,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
args.add(ctx.lowerExpr(idxExpr))
args.add(ctx.lowerExpr(expr.exprAssignValue))
return hirCall(calleeName, args, makeVoid(), loc)
# `*p = value` must store through the pointer, not assign to a loaded temp.
# Represent as hAssign to hLoad(loadPtr=p) so LIR emits `*p = value`.
if expr.exprAssignTarget.kind == ekUnary and expr.exprAssignTarget.exprUnaryOp == tkStar:
let destPtr = ctx.lowerExpr(expr.exprAssignTarget.exprUnaryOperand)
let value = ctx.lowerExpr(expr.exprAssignValue)
let loadTarget = HirNode(kind: hLoad, loadPtr: destPtr, typ: typ, loc: loc)
return HirNode(kind: hAssign, assignOp: tkAssign,
assignTarget: loadTarget, assignValue: value,
typ: makeVoid(), loc: loc)
let target = ctx.lowerExpr(expr.exprAssignTarget)
let value = ctx.lowerExpr(expr.exprAssignValue)
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
@@ -1473,29 +1600,30 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekMatch:
let subject = ctx.lowerExpr(expr.exprMatchSubject)
var subjectEnumName = ""
var subjectHasData = false
if subject.typ != nil and subject.typ.kind == tkNamed:
subjectEnumName = subject.typ.name
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
# Prefer resolved match type; fall back to function return type when arms
# only reference pattern bindings (not yet in varTypeExprs during resolve).
var matchTyp = typ
if matchTyp == nil or matchTyp.kind == tkUnknown:
if ctx.currentFuncRetType != nil and ctx.currentFuncRetType.kind notin {tkVoid, tkUnknown}:
matchTyp = ctx.currentFuncRetType
var arms: seq[HirMatchArm] = @[]
# Register bind types early so matchTyp fallback can resolve arm bodies
var subjectEnumName = ""
var subjectHasData = false
if subject.typ != nil and subject.typ.kind == tkNamed:
subjectEnumName = subject.typ.name
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
for arm in expr.exprMatchArms:
# Register bind types only (do NOT call matchPatternBindings here — that
# would mark names as already alloca'd and lowerMatch would skip them).
if arm.pattern != nil and arm.pattern.kind == pkEnum and subjectHasData:
var bindPat = arm.pattern
if bindPat != nil and bindPat.kind == pkGuarded:
bindPat = bindPat.patGuardedInner
if bindPat != nil and bindPat.kind == pkEnum and subjectHasData:
var enumName = ""
var variantName = ""
if arm.pattern.patEnumPath.len >= 2:
enumName = arm.pattern.patEnumPath[0]
variantName = arm.pattern.patEnumPath[^1]
elif arm.pattern.patEnumPath.len == 1:
variantName = arm.pattern.patEnumPath[0]
if bindPat.patEnumPath.len >= 2:
enumName = bindPat.patEnumPath[0]
variantName = bindPat.patEnumPath[^1]
elif bindPat.patEnumPath.len == 1:
variantName = bindPat.patEnumPath[0]
enumName = subjectEnumName
var fieldTypes: seq[Type] = @[]
let enumSym = ctx.globalScope.lookup(enumName)
@@ -1505,19 +1633,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
for f in v.fields:
fieldTypes.add(ctx.resolveTypeExpr(f))
break
for i, arg in arm.pattern.patEnumArgs:
for i, arg in bindPat.patEnumArgs:
if arg != nil and arg.kind == pkIdent:
let ft = if i < fieldTypes.len: fieldTypes[i] else: makeInt()
ctx.varTypeExprs[arg.patIdent] = typeToTypeExpr(ft)
elif arm.pattern != nil and arm.pattern.kind == pkIdent:
elif bindPat != nil and bindPat.kind == pkIdent:
let ty = if subject.typ != nil: subject.typ else: makeUnknown()
ctx.varTypeExprs[arm.pattern.patIdent] = typeToTypeExpr(ty)
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body)))
# Re-resolve match type now that bindings are registered
if matchTyp == nil or matchTyp.kind == tkUnknown:
if arms.len > 0 and arms[0].body != nil and arms[0].body.typ != nil:
matchTyp = arms[0].body.typ
return lowerMatch(ctx, subject, arms, matchTyp, loc)
ctx.varTypeExprs[bindPat.patIdent] = typeToTypeExpr(ty)
# Binds + body lower happen inside lowerMatch (unique C names + renames)
return lowerMatch(ctx, subject, expr.exprMatchArms, matchTyp, loc)
of ekSizeOf:
let ty = ctx.resolveTypeExpr(expr.exprSizeOfType)
@@ -1663,6 +1787,13 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if initHir != nil:
let store = hirStore(varNode, initHir, loc)
stmts.add(store)
# Auto-Drop: @[Drop] types and Array/Map/etc. with TypeName_Drop
let dropName = ctx.autoDropFuncName(allocaType)
if dropName.len > 0:
let addrOf = hirUnary(tkAmp, hirVar(stmt.stmtLetName, allocaType, loc),
makePointer(allocaType), loc)
let dropCall = hirCall(dropName, @[addrOf], makeVoid(), loc)
ctx.deferStmts.add(dropCall)
# Capture filling for closures is done at the ekClosure site (heap env).
return hirBlock(stmts, nil, makeVoid(), loc)
@@ -1670,10 +1801,25 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil
var stmts = ctx.pendingStmts
ctx.pendingStmts = @[]
# Add defers in reverse order (LIFO)
# Move-on-return: do not Drop a local that is returned by value.
var skipDrop = ""
if value != nil and value.kind == hVar:
skipDrop = value.varName
# Materialize the return value BEFORE drops so `return a.id` is not
# use-after-drop (drops are separate stmts; LIR evaluates return expr last).
var retVal = value
if value != nil and ctx.deferStmts.len > 0:
let retTy = if value.typ != nil: value.typ else: makeUnknown()
if retTy.kind != tkVoid:
let tmp = ctx.freshName()
stmts.add(hirAlloca(tmp, retTy, loc))
stmts.add(hirStore(hirVar(tmp, retTy, loc), value, loc))
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):
stmts.add(ctx.deferStmts[i])
stmts.add(hirReturn(value, loc))
if not dropTargetsVar(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i])
stmts.add(hirReturn(retVal, loc))
return hirBlock(stmts, nil, makeVoid(), loc)
of skIf:
@@ -1904,11 +2050,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
of skMatch:
let subject = ctx.lowerExpr(stmt.stmtMatchSubject)
var arms: seq[HirMatchArm] = @[]
for arm in stmt.stmtMatchArms:
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body)))
# Statement match: lower to if-else chain (void result)
return ctx.flushPending(lowerMatch(ctx, subject, arms, makeVoid(), loc))
# Statement match: binds + body lower inside lowerMatch (unique C names)
return ctx.flushPending(lowerMatch(ctx, subject, stmt.stmtMatchArms, makeVoid(), loc))
of skSwitch:
let subject = ctx.lowerExpr(stmt.stmtSwitchExpr)
@@ -1941,7 +2084,13 @@ 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)
## keep asExpr=false so trailing void calls stay as statements.
##
## Auto-drop / defer scope: locals introduced in this block are dropped at
## block exit (LIFO). Nested if/while bodies get their own scope so branch-
## local drops do not leak into sibling branches. Early return still injects
## the full live stack (see skReturn).
if blk == nil: return nil
let deferBase = ctx.deferStmts.len
var stmts: seq[HirNode] = @[]
for s in blk.stmts:
let hir = ctx.lowerStmt(s)
@@ -1966,6 +2115,16 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
let last = stmts[^1]
stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc)
expr = last.blockExpr
# Scope exit: Drop locals introduced in this block (not outer ones).
# Skip Drop for a local that is the block result (move into expr / caller).
var skipDrop = ""
if expr != nil and expr.kind == hVar:
skipDrop = expr.varName
if ctx.deferStmts.len > deferBase:
for i in countdown(ctx.deferStmts.len - 1, deferBase):
if not dropTargetsVar(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i])
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)
@@ -2008,10 +2167,14 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
let oldFuncRetType = ctx.currentFuncRetType
let oldVarTypeExprs = ctx.varTypeExprs
let oldPatternBound = ctx.patternBoundNames
let oldPatternRenames = ctx.patternRenames
ctx.currentFuncRetType = retType
ctx.currentFuncDecl = decl
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
ctx.patternBoundNames = initHashSet[string]()
ctx.patternRenames = initTable[string, string]()
let oldDefers = ctx.deferStmts
ctx.deferStmts = @[]
# Add parameters to varTypeExprs after clearing so they are visible in the body.
for p in funcParams:
if p.ptype != nil:
@@ -2032,12 +2195,13 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
if not hasReturn:
for i in countdown(ctx.deferStmts.len - 1, 0):
body.blockStmts.add(ctx.deferStmts[i])
ctx.deferStmts = @[]
ctx.deferStmts = oldDefers
ctx.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType
ctx.varTypeExprs = oldVarTypeExprs
ctx.patternBoundNames = oldPatternBound
ctx.patternRenames = oldPatternRenames
result = HirFunc(name: funcName, params: params, retType: retType,
body: body, isPublic: decl.isPublic)