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
+18
View File
@@ -142,6 +142,24 @@ struct Expr {
structFieldCount: int,
// Closure params (for ekClosure)
closureParams: *Decl,
// Captures (for ekClosure)
captureCount: int,
captureName0: String,
captureName1: String,
captureName2: String,
captureName3: String,
captureName4: String,
captureName5: String,
captureName6: String,
captureName7: String,
captureType0: int,
captureType1: int,
captureType2: int,
captureType3: int,
captureType4: int,
captureType5: int,
captureType6: int,
captureType7: int,
// Call arguments (linked list for multi-arg support)
callArgs: *ExprList,
callArgCount: int,
+45
View File
@@ -445,6 +445,14 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
return;
}
// Field access on value (direct dot): obj.field
if kind == hFieldAccess {
CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, ".");
StringBuilder_Append(&cbe.sb, node.strValue);
return;
}
// Field access: obj.field — use -> if base is pointer, else .
if kind == hFieldPtr {
CBE_EmitExpr(cbe, node.child1);
@@ -1042,6 +1050,43 @@ func CBackend_Generate(mod: *HirModule) -> String {
i = i + 1;
continue;
}
// Emit env struct and global instance for closures with captures
if mod.funcs[i].captureCount > 0 {
let envName: String = mod.funcs[i].envStructName;
let instName: String = mod.funcs[i].envInstanceName;
if !String_Eq(envName, "") {
// Emit struct definition
StringBuilder_Append(&cbe.sb, "struct ");
StringBuilder_Append(&cbe.sb, envName);
StringBuilder_Append(&cbe.sb, " {\n");
var ci: int = 0;
while ci < mod.funcs[i].captureCount {
var capName: String = "";
var capType: String = "int";
if ci == 0 { capName = mod.funcs[i].captureName0; capType = CBackend_TypeToC(mod.funcs[i].captureType0); }
else if ci == 1 { capName = mod.funcs[i].captureName1; capType = CBackend_TypeToC(mod.funcs[i].captureType1); }
else if ci == 2 { capName = mod.funcs[i].captureName2; capType = CBackend_TypeToC(mod.funcs[i].captureType2); }
else if ci == 3 { capName = mod.funcs[i].captureName3; capType = CBackend_TypeToC(mod.funcs[i].captureType3); }
else if ci == 4 { capName = mod.funcs[i].captureName4; capType = CBackend_TypeToC(mod.funcs[i].captureType4); }
else if ci == 5 { capName = mod.funcs[i].captureName5; capType = CBackend_TypeToC(mod.funcs[i].captureType5); }
else if ci == 6 { capName = mod.funcs[i].captureName6; capType = CBackend_TypeToC(mod.funcs[i].captureType6); }
else if ci == 7 { capName = mod.funcs[i].captureName7; capType = CBackend_TypeToC(mod.funcs[i].captureType7); }
StringBuilder_Append(&cbe.sb, " ");
StringBuilder_Append(&cbe.sb, capType);
StringBuilder_Append(&cbe.sb, " ");
StringBuilder_Append(&cbe.sb, capName);
StringBuilder_Append(&cbe.sb, ";\n");
ci = ci + 1;
}
StringBuilder_Append(&cbe.sb, "};\n");
// Emit global instance
StringBuilder_Append(&cbe.sb, "static struct ");
StringBuilder_Append(&cbe.sb, envName);
StringBuilder_Append(&cbe.sb, " ");
StringBuilder_Append(&cbe.sb, instName);
StringBuilder_Append(&cbe.sb, ";\n\n");
}
}
CBE_EmitFuncDecl(cbe, &mod.funcs[i]);
StringBuilder_Append(&cbe.sb, " {\n");
// Body
+23 -2
View File
@@ -18,8 +18,9 @@ const hAlloca: int = 12;
const hLoad: int = 13;
const hStore: int = 14;
const hFieldPtr: int = 15;
const hArrowField: int = 16;
const hIndexPtr: int = 17;
const hFieldAccess: int = 16;
const hArrowField: int = 17;
const hIndexPtr: int = 18;
const hCall: int = 18;
const hCallIndirect: int = 19;
const hCast: int = 20;
@@ -93,6 +94,26 @@ struct HirFunc {
retTypeName: String;
body: *HirNode;
isPublic: bool;
// Closure capture metadata
captureCount: int;
captureName0: String;
captureName1: String;
captureName2: String;
captureName3: String;
captureName4: String;
captureName5: String;
captureName6: String;
captureName7: String;
captureType0: int;
captureType1: int;
captureType2: int;
captureType3: int;
captureType4: int;
captureType5: int;
captureType6: int;
captureType7: int;
envStructName: String;
envInstanceName: String;
}
// ---------------------------------------------------------------------------
+167 -4
View File
@@ -15,6 +15,10 @@ struct LowerCtx {
externCount: int,
varCounter: int,
tryCounter: int,
closureDepth: int,
currentClosureExpr: *Expr,
envInstanceName: String,
hm: *HirModule,
}
// ---------------------------------------------------------------------------
@@ -50,6 +54,29 @@ func Lcx_ResolveTypeKindFromName(name: String) -> int {
return tyNamed;
}
func Lcx_TypeKindToName(kind: int) -> String {
if kind == tyVoid { return "void"; }
if kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32 { return "bool"; }
if kind == tyChar8 { return "char"; }
if kind == tyChar16 { return "uint16"; }
if kind == tyChar32 { return "uint32"; }
if kind == tyStr { return "String"; }
if kind == tyInt8 { return "int8"; }
if kind == tyInt16 { return "int16"; }
if kind == tyInt32 { return "int32"; }
if kind == tyInt64 { return "int64"; }
if kind == tyInt { return "int"; }
if kind == tyUInt8 { return "uint8"; }
if kind == tyUInt16 { return "uint16"; }
if kind == tyUInt32 { return "uint32"; }
if kind == tyUInt64 { return "uint64"; }
if kind == tyUInt { return "uint"; }
if kind == tyFloat32 { return "float32"; }
if kind == tyFloat64 { return "float64"; }
if kind == tyPointer { return "void*"; }
return "int";
}
func Lcx_ResolveTypeKind(te: *TypeExpr) -> int {
if te == null as *TypeExpr { return tyUnknown; }
@@ -122,6 +149,40 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
// Identifier → variable reference
if kind == ekIdent {
// Capture rewriting: if inside closure body and this ident is captured,
// emit field access on env instance instead of bare variable
if ctx.closureDepth > 0 && ctx.currentClosureExpr != null as *Expr && !String_Eq(ctx.envInstanceName, "") {
let capCount: int = ctx.currentClosureExpr.captureCount;
var ci: int = 0;
var isCaptured: bool = false;
var capType: int = 0;
while ci < capCount {
var capName: String = "";
if ci == 0 { capName = ctx.currentClosureExpr.captureName0; capType = ctx.currentClosureExpr.captureType0; }
else if ci == 1 { capName = ctx.currentClosureExpr.captureName1; capType = ctx.currentClosureExpr.captureType1; }
else if ci == 2 { capName = ctx.currentClosureExpr.captureName2; capType = ctx.currentClosureExpr.captureType2; }
else if ci == 3 { capName = ctx.currentClosureExpr.captureName3; capType = ctx.currentClosureExpr.captureType3; }
else if ci == 4 { capName = ctx.currentClosureExpr.captureName4; capType = ctx.currentClosureExpr.captureType4; }
else if ci == 5 { capName = ctx.currentClosureExpr.captureName5; capType = ctx.currentClosureExpr.captureType5; }
else if ci == 6 { capName = ctx.currentClosureExpr.captureName6; capType = ctx.currentClosureExpr.captureType6; }
else if ci == 7 { capName = ctx.currentClosureExpr.captureName7; capType = ctx.currentClosureExpr.captureType7; }
if String_Eq(capName, expr.strValue) {
isCaptured = true;
}
ci = ci + 1;
}
if isCaptured {
n.kind = hFieldAccess;
n.strValue = expr.strValue;
let baseNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
baseNode.kind = hVar;
baseNode.strValue = ctx.envInstanceName;
n.child1 = baseNode;
n.typeKind = capType;
n.typeName = Lcx_TypeKindToName(capType);
return n;
}
}
n.kind = hVar;
n.strValue = expr.strValue;
let sym: Symbol = Scope_Lookup(ctx.scope, expr.strValue);
@@ -500,10 +561,71 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
sym.decl = null as *Decl;
discard Scope_Define(ctx.scope, sym);
// store the init value
n.kind = hStore;
n.child1 = alloca;
n.child2 = init;
return n;
let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
storeNode.kind = hStore;
storeNode.child1 = alloca;
storeNode.child2 = init;
// If init is a closure with captures, emit capture assignments before the let
if stmt.child1 != null as *Expr && stmt.child1.kind == ekClosure && stmt.child1.captureCount > 0 {
let closureIdx: int = ctx.funcCount - 1;
let envInst: String = String_Concat("__closure_env_instance_", String_FromInt(closureIdx));
// Build a block: capture assignments + let store
let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
blockNode.kind = hBlock;
blockNode.line = line;
blockNode.column = col;
var firstStmt: *HirNode = null as *HirNode;
var lastStmt: *HirNode = null as *HirNode;
var ci: int = 0;
while ci < stmt.child1.captureCount {
var capName: String = "";
if ci == 0 { capName = stmt.child1.captureName0; }
else if ci == 1 { capName = stmt.child1.captureName1; }
else if ci == 2 { capName = stmt.child1.captureName2; }
else if ci == 3 { capName = stmt.child1.captureName3; }
else if ci == 4 { capName = stmt.child1.captureName4; }
else if ci == 5 { capName = stmt.child1.captureName5; }
else if ci == 6 { capName = stmt.child1.captureName6; }
else if ci == 7 { capName = stmt.child1.captureName7; }
// Build: envInst.capName = capName;
let assignNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
assignNode.kind = hAssign;
assignNode.line = line;
assignNode.column = col;
let fieldNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
fieldNode.kind = hFieldAccess;
fieldNode.strValue = capName;
let baseNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
baseNode.kind = hVar;
baseNode.strValue = envInst;
fieldNode.child1 = baseNode;
let valNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
valNode.kind = hVar;
valNode.strValue = capName;
assignNode.child1 = fieldNode;
assignNode.child2 = valNode;
if firstStmt == null as *HirNode {
firstStmt = assignNode;
lastStmt = assignNode;
} else {
lastStmt.child3 = assignNode;
lastStmt = assignNode;
}
ci = ci + 1;
}
// Append the let store
if firstStmt == null as *HirNode {
firstStmt = storeNode;
lastStmt = storeNode;
} else {
lastStmt.child3 = storeNode;
lastStmt = storeNode;
}
blockNode.child1 = firstStmt;
return blockNode;
}
return storeNode;
}
// Return
@@ -830,6 +952,37 @@ func Lcx_LowerClosureFunc(ctx: *LowerCtx, expr: *Expr) -> *HirFunc {
f.retTypeKind = 0;
}
// Copy capture metadata from AST
f.captureCount = expr.captureCount;
f.captureName0 = expr.captureName0;
f.captureName1 = expr.captureName1;
f.captureName2 = expr.captureName2;
f.captureName3 = expr.captureName3;
f.captureName4 = expr.captureName4;
f.captureName5 = expr.captureName5;
f.captureName6 = expr.captureName6;
f.captureName7 = expr.captureName7;
f.captureType0 = expr.captureType0;
f.captureType1 = expr.captureType1;
f.captureType2 = expr.captureType2;
f.captureType3 = expr.captureType3;
f.captureType4 = expr.captureType4;
f.captureType5 = expr.captureType5;
f.captureType6 = expr.captureType6;
f.captureType7 = expr.captureType7;
// Generate env struct and instance names if there are captures
var envStructName: String = "";
var envInstanceName: String = "";
if f.captureCount > 0 {
envStructName = String_Concat("__closure_env_", numStr);
envInstanceName = String_Concat("__closure_env_instance_", numStr);
f.envStructName = envStructName;
f.envInstanceName = envInstanceName;
// Env struct is emitted directly by C backend from func capture metadata
}
// Create function scope
var funcScope: Scope = Scope_NewChild(ctx.scope);
var pi: int = 0;
@@ -868,13 +1021,22 @@ func Lcx_LowerClosureFunc(ctx: *LowerCtx, expr: *Expr) -> *HirFunc {
}
let prevScope: *Scope = ctx.scope;
let prevClosureDepth: int = ctx.closureDepth;
let prevClosureExpr: *Expr = ctx.currentClosureExpr;
let prevEnvInstanceName: String = ctx.envInstanceName;
ctx.scope = &funcScope;
ctx.closureDepth = ctx.closureDepth + 1;
ctx.currentClosureExpr = expr;
ctx.envInstanceName = envInstanceName;
if expr.refBlock != null as *Block {
f.body = Lcx_LowerBlock(ctx, expr.refBlock, -1);
} else {
f.body = null as *HirNode;
}
ctx.scope = prevScope;
ctx.closureDepth = prevClosureDepth;
ctx.currentClosureExpr = prevClosureExpr;
ctx.envInstanceName = prevEnvInstanceName;
// Add to module functions
ctx.funcs[ctx.funcCount] = *f;
@@ -906,6 +1068,7 @@ func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule {
hm.enums = bux_alloc(64 as uint * sizeof(HirEnum)) as *HirEnum;
hm.constCount = 0;
hm.consts = bux_alloc(512 as uint * sizeof(HirConst)) as *HirConst;
ctx.hm = hm;
// First pass: count structs (to allocate field arrays later)
// Second pass: actually collect them
+19
View File
@@ -89,6 +89,25 @@ func Scope_LookupLocal(scope: *Scope, name: String) -> Symbol {
return empty;
}
func Scope_LookupUpTo(scope: *Scope, name: String, limit: *Scope) -> Symbol {
var cur: *Scope = scope;
while cur != null as *Scope {
var i: int = 0;
while i < cur.count {
if String_Eq(cur.symbols[i].name, name) {
return cur.symbols[i];
}
i = i + 1;
}
if cur == limit {
break;
}
cur = cur.parent;
}
var empty: Symbol = Symbol { kind: 0, name: "", typeKind: 0, typeName: "", refType: null as *TypeExpr, isMutable: false, isPublic: false, decl: null as *Decl };
return empty;
}
func Scope_Free(scope: *Scope) {
bux_free(scope.symbols as *void);
}
+61
View File
@@ -24,6 +24,9 @@ struct Sema {
movedName5: String;
movedName6: String;
movedName7: String;
closureDepth: int; // nesting depth inside closures
currentClosureExpr: *Expr; // current closure being analyzed (for capture tracking)
closureScope: *Scope; // scope at which the current closure was entered
}
struct SemaDiag {
@@ -337,6 +340,42 @@ func Sema_RemoveMoved(sema: *Sema, name: String) {
}
}
// ---------------------------------------------------------------------------
// Capture tracking for closures
// ---------------------------------------------------------------------------
func Sema_AddCapture(closureExpr: *Expr, name: String, typeKind: int) {
if closureExpr == null as *Expr { return; }
// Check if already captured
var i: int = 0;
while i < closureExpr.captureCount {
var capName: String = "";
if i == 0 { capName = closureExpr.captureName0; }
else if i == 1 { capName = closureExpr.captureName1; }
else if i == 2 { capName = closureExpr.captureName2; }
else if i == 3 { capName = closureExpr.captureName3; }
else if i == 4 { capName = closureExpr.captureName4; }
else if i == 5 { capName = closureExpr.captureName5; }
else if i == 6 { capName = closureExpr.captureName6; }
else if i == 7 { capName = closureExpr.captureName7; }
if String_Eq(capName, name) { return; }
i = i + 1;
}
// Add new capture (up to 8)
if closureExpr.captureCount < 8 {
let idx: int = closureExpr.captureCount;
if idx == 0 { closureExpr.captureName0 = name; closureExpr.captureType0 = typeKind; }
else if idx == 1 { closureExpr.captureName1 = name; closureExpr.captureType1 = typeKind; }
else if idx == 2 { closureExpr.captureName2 = name; closureExpr.captureType2 = typeKind; }
else if idx == 3 { closureExpr.captureName3 = name; closureExpr.captureType3 = typeKind; }
else if idx == 4 { closureExpr.captureName4 = name; closureExpr.captureType4 = typeKind; }
else if idx == 5 { closureExpr.captureName5 = name; closureExpr.captureType5 = typeKind; }
else if idx == 6 { closureExpr.captureName6 = name; closureExpr.captureType6 = typeKind; }
else if idx == 7 { closureExpr.captureName7 = name; closureExpr.captureType7 = typeKind; }
closureExpr.captureCount = closureExpr.captureCount + 1;
}
}
// ---------------------------------------------------------------------------
// Expression type checking
// ---------------------------------------------------------------------------
@@ -379,6 +418,16 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
te.typeName = sym.typeName;
expr.refType = te;
}
// Capture tracking: if inside closure and identifier is not local but from parent scope
if sema.closureDepth > 0 && sema.currentClosureExpr != null as *Expr && sema.closureScope != null as *Scope {
let localSym: Symbol = Scope_LookupUpTo(sema.scope, expr.strValue, sema.closureScope);
if localSym.kind == 0 && !String_Eq(localSym.name, expr.strValue) {
// Not local to closure scope — check if it's a variable from outer scope
if sym.kind == skVar {
Sema_AddCapture(sema.currentClosureExpr, expr.strValue, sym.typeKind);
}
}
}
return sym.typeKind;
}
@@ -568,9 +617,18 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
if kind == ekClosure {
let savedRetType: int = sema.currentRetType;
let savedScope: *Scope = sema.scope;
let savedClosureDepth: int = sema.closureDepth;
let savedClosureExpr: *Expr = sema.currentClosureExpr;
let savedClosureScope: *Scope = sema.closureScope;
let childScope: Scope = Scope_NewChild(sema.scope);
sema.scope = &childScope;
// Set up closure tracking
sema.closureDepth = sema.closureDepth + 1;
sema.currentClosureExpr = expr;
sema.closureScope = &childScope;
expr.captureCount = 0;
// Set return type from annotation, or unknown for inference
var closureRetType: int = tyUnknown;
if expr.refType != null as *TypeExpr {
@@ -621,6 +679,9 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
// Restore
sema.scope = savedScope;
sema.currentRetType = savedRetType;
sema.closureDepth = savedClosureDepth;
sema.currentClosureExpr = savedClosureExpr;
sema.closureScope = savedClosureScope;
// Build function type expression for later use
let funcType: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;