closures with captures: env struct + global instance + body rewriting

- AST: captureCount + captureName0..7 + captureType0..7 in Expr
- Sema: Scope_LookupUpTo + closureScope for capture analysis
- HIR Lower: env struct generation, capture assignments in skLet,
  body rewriting (ekIdent -> hFieldAccess on env instance)
- C Backend: env struct def + global instance emission for closures
- Bootstrap: full capture support in sema, hir_lower, lir_lower, lir_c_backend
- Selfhost loop remains deterministic
This commit is contained in:
2026-06-09 21:23:05 +03:00
parent ba54aa240e
commit 9b473c667c
17 changed files with 5267 additions and 53 deletions
+3
View File
@@ -233,6 +233,9 @@ type
exprClosureParams*: seq[Param]
exprClosureBody*: Block
exprClosureReturnType*: TypeExpr
captureCount*: int
captureNames*: seq[string]
captureTypeKinds*: seq[int]
# ---------------------------------------------------------------------------
# Statements
+16
View File
@@ -272,6 +272,10 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
let base = be.emitExpr(node.fieldPtrBase)
return &"(&({base}.{node.fieldName}))"
of hFieldAccess:
let base = be.emitExpr(node.fieldAccessBase)
return &"({base}.{node.fieldAccessName})"
of hArrowField:
let base = be.emitExpr(node.arrowFieldBase)
return &"(&({base}->{node.arrowFieldName}))"
@@ -476,6 +480,18 @@ proc emitStmt(be: var CBackend, node: HirNode) =
be.emitLine(&"{expr};")
proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
# Emit env struct and global instance for closures with captures
if hfunc.captureNames.len > 0 and hfunc.envStructName != "":
be.emitLine(&"struct {hfunc.envStructName} {{")
inc be.indent
for i in 0 ..< hfunc.captureNames.len:
let capName = hfunc.captureNames[i]
let capType = if i < hfunc.captureTypes.len: typeToC(be, hfunc.captureTypes[i]) else: "int"
be.emitLine(&"{capType} {capName};")
dec be.indent
be.emitLine("};")
be.emitLine(&"static struct {hfunc.envStructName} {hfunc.envInstanceName};")
be.emitLine("")
let retType = typeToC(be, hfunc.retType)
var params: seq[string] = @[]
for p in hfunc.params:
+12
View File
@@ -23,6 +23,7 @@ type
hLoad
hStore
hFieldPtr
hFieldAccess
hArrowField
hIndexPtr
hSliceIndex
@@ -98,6 +99,9 @@ type
of hFieldPtr:
fieldPtrBase*: HirNode
fieldName*: string
of hFieldAccess:
fieldAccessBase*: HirNode
fieldAccessName*: string
of hArrowField:
arrowFieldBase*: HirNode
arrowFieldName*: string
@@ -166,6 +170,11 @@ type
retType*: Type
body*: HirNode
isPublic*: bool
# Closure capture metadata
captureNames*: seq[string]
captureTypes*: seq[Type]
envStructName*: string
envInstanceName*: string
HirEnumVariant* = object
name*: string
@@ -219,6 +228,9 @@ proc hirAlloca*(name: string, typ: Type, loc: SourceLocation): HirNode =
proc hirStore*(ptrNode, value: HirNode, loc: SourceLocation): HirNode =
HirNode(kind: hStore, storePtr: ptrNode, storeValue: value, typ: makeVoid(), loc: loc)
proc hirAssign*(target, value: HirNode, loc: SourceLocation): HirNode =
HirNode(kind: hAssign, assignOp: tkAssign, assignTarget: target, assignValue: value, typ: makeVoid(), loc: loc)
proc hirLoad*(ptrNode: HirNode, typ: Type, loc: SourceLocation): HirNode =
HirNode(kind: hLoad, loadPtr: ptrNode, typ: typ, loc: loc)
+43 -1
View File
@@ -22,6 +22,9 @@ type
generatedFuncInsts*: Table[string, bool] # Track generated function instantiations
extraFuncs*: seq[HirFunc] # Monomorphized generic methods
varTypeExprs*: Table[string, TypeExpr] # Track variable names -> type expr for generic method inference
closureDepth*: int
currentClosureExpr*: Expr
envInstanceName*: string
proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter
@@ -648,6 +651,13 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekIdent:
let name = expr.exprIdent
# 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)
if idx >= 0:
let capType = if idx < ctx.currentClosureExpr.captureTypeKinds.len: Type(kind: TypeKind(ctx.currentClosureExpr.captureTypeKinds[idx])) else: makeInt()
let base = hirVar(ctx.envInstanceName, makeNamed(""), loc)
return HirNode(kind: hFieldAccess, fieldAccessName: name, fieldAccessBase: base, typ: capType, loc: loc)
if ctx.importTable.hasKey(name):
return hirVar(ctx.importTable[name], typ, loc)
return hirVar(name, typ, loc)
@@ -1202,6 +1212,22 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if initHir != nil:
let store = hirStore(varNode, initHir, loc)
stmts.add(store)
# If init is a closure with captures, emit capture assignments
if stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekClosure and stmt.stmtLetInit.captureCount > 0:
let closureIdx = ctx.varCounter - 1
let envInst = "__closure_env_instance_" & $closureIdx
var capStmts: seq[HirNode] = @[]
for i in 0 ..< stmt.stmtLetInit.captureCount:
let capName = stmt.stmtLetInit.captureNames[i]
let capType = if i < stmt.stmtLetInit.captureTypeKinds.len: Type(kind: TypeKind(stmt.stmtLetInit.captureTypeKinds[i])) else: makeInt()
let base = hirVar(envInst, makeNamed(""), loc)
let field = HirNode(kind: hFieldAccess, fieldAccessName: capName, fieldAccessBase: base, typ: capType, loc: loc)
let val = hirVar(capName, capType, loc)
capStmts.add(hirAssign(field, val, loc))
# Prepend capture assignments before the let
var allStmts = capStmts
allStmts.add(stmts)
return hirBlock(allStmts, nil, makeVoid(), loc)
return hirBlock(stmts, nil, makeVoid(), loc)
of skReturn:
@@ -1491,6 +1517,13 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
let name = "__closure_" & $ctx.varCounter
inc ctx.varCounter
var f = HirFunc(name: name, isPublic: false)
# Copy capture metadata
if expr.captureCount > 0:
f.captureNames = expr.captureNames
for tk in expr.captureTypeKinds:
f.captureTypes.add(Type(kind: TypeKind(tk)))
f.envStructName = "__closure_env_" & $(ctx.varCounter - 1)
f.envInstanceName = "__closure_env_instance_" & $(ctx.varCounter - 1)
# Params
for p in expr.exprClosureParams:
f.params.add((name: p.name, typ: if p.ptype != nil: ctx.resolveTypeExpr(p.ptype) else: makeUnknown()))
@@ -1499,9 +1532,18 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
f.retType = ctx.resolveTypeExpr(expr.exprClosureReturnType)
else:
f.retType = makeVoid()
# Body
# Body with closure rewriting
let savedDepth = ctx.closureDepth
let savedExpr = ctx.currentClosureExpr
let savedEnv = ctx.envInstanceName
ctx.closureDepth = ctx.closureDepth + 1
ctx.currentClosureExpr = expr
ctx.envInstanceName = f.envInstanceName
if expr.exprClosureBody != nil:
f.body = ctx.lowerBlock(expr.exprClosureBody)
ctx.closureDepth = savedDepth
ctx.currentClosureExpr = savedExpr
ctx.envInstanceName = savedEnv
ctx.extraFuncs.add(f)
return f
+14
View File
@@ -626,6 +626,20 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
be.emitLine("};")
be.emitLine("")
# Emit env structs for closures with captures
for f in module.funcs:
if f.captureNames.len > 0 and f.envStructName != "":
be.emitLine(&"struct {f.envStructName} {{")
be.indent += 1
for i in 0 ..< f.captureNames.len:
let capName = f.captureNames[i]
let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int"
be.emitLine(&"{capType} {capName};")
be.indent -= 1
be.emitLine("};")
be.emitLine(&"static struct {f.envStructName} {f.envInstanceName};")
be.emitLine("")
# Emit all LIR functions
for f in builder.funcs:
be.emitFunc(f, funcRetTypes, funcPtrTypes)
+14
View File
@@ -345,6 +345,14 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
b.emitRawC(&"{t.strVal} = (void*)&({lirValToC(base)}.{node.fieldName});")
return t
of hFieldAccess:
let base = lowerExpr(ctx, node.fieldAccessBase)
let cType = hirTypeToC(ctx, node)
let t = b.freshTemp()
b.emitAlloca(t.strVal, cType)
b.emitRawC(&"{t.strVal} = {lirValToC(base)}.{node.fieldAccessName};")
return t
of hArrowField:
let base = lowerExpr(ctx, node.arrowFieldBase)
let t = b.freshTemp()
@@ -549,6 +557,9 @@ proc buildLval(ctx: var LowerToLirCtx, n: HirNode): string =
of hArrowField:
let baseStr = buildLval(ctx, n.arrowFieldBase)
return baseStr & "->" & n.arrowFieldName
of hFieldAccess:
let baseStr = buildLval(ctx, n.fieldAccessBase)
return baseStr & "." & n.fieldAccessName
of hIndexPtr:
let baseStr = buildLval(ctx, n.indexPtrBase)
let idx = lowerExpr(ctx, n.indexPtrIndex)
@@ -669,6 +680,9 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
of hFieldPtr:
let lval = buildLval(ctx, node.assignTarget)
b.emitRawC(&"{lval} = {lirValToC(value)};")
of hFieldAccess:
let lval = buildLval(ctx, node.assignTarget)
b.emitRawC(&"{lval} = {lirValToC(value)};")
of hArrowField:
let lval = buildLval(ctx, node.assignTarget)
b.emitRawC(&"{lval} = {lirValToC(value)};")
+10
View File
@@ -43,4 +43,14 @@ proc lookup*(scope: Scope, name: string): Symbol =
proc lookupLocal*(scope: Scope, name: string): Symbol =
if scope.table.hasKey(name):
return scope.table[name]
return nil
proc lookupUpTo*(scope: Scope, name: string, limit: Scope): Symbol =
var cur = scope
while cur != nil:
if cur.table.hasKey(name):
return cur.table[name]
if cur == limit:
break
cur = cur.parent
return nil
+23
View File
@@ -45,6 +45,9 @@ type
currentFuncIsAsync*: bool ## true inside async func
movedVars*: seq[string] ## variables moved in current checked function
currentRetType*: Type ## return type of the function being checked
closureDepth*: int ## nesting depth inside closures
currentClosureExpr*: Expr ## current closure being analyzed
closureScope*: Scope ## scope at which the current closure was entered
# ---------------------------------------------------------------------------
# Helpers
@@ -816,6 +819,14 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
return makeUnknown()
if sym.typ == nil:
return makeUnknown()
# Capture tracking
if sema.closureDepth > 0 and sema.currentClosureExpr != nil and sema.closureScope != nil:
let localSym = scope.lookupUpTo(expr.exprIdent, sema.closureScope)
if localSym == nil and sym.kind == skVar:
if expr.exprIdent notin sema.currentClosureExpr.captureNames:
sema.currentClosureExpr.captureNames.add(expr.exprIdent)
sema.currentClosureExpr.captureTypeKinds.add(sym.typ.kind.int)
sema.currentClosureExpr.captureCount = sema.currentClosureExpr.captureNames.len
return sym.typ
of ekSelf:
let sym = scope.lookup("self")
@@ -1340,7 +1351,16 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
return makeStr()
of ekClosure:
let savedRetType = sema.currentRetType
let savedClosureDepth = sema.closureDepth
let savedClosureExpr = sema.currentClosureExpr
let savedClosureScope = sema.closureScope
let childScope = Scope(parent: scope)
sema.closureDepth = sema.closureDepth + 1
sema.currentClosureExpr = expr
sema.closureScope = childScope
expr.captureCount = 0
expr.captureNames = @[]
expr.captureTypeKinds = @[]
sema.currentRetType = if expr.exprClosureReturnType != nil: sema.resolveType(expr.exprClosureReturnType) else: makeUnknown()
# Register params
for p in expr.exprClosureParams:
@@ -1351,6 +1371,9 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
for stmt in expr.exprClosureBody.stmts:
discard sema.checkStmt(stmt, childScope)
sema.currentRetType = savedRetType
sema.closureDepth = savedClosureDepth
sema.currentClosureExpr = savedClosureExpr
sema.closureScope = savedClosureScope
# Build function type
var params: seq[Type] = @[]
for p in expr.exprClosureParams: