fix(bootstrap): implement collection for-in lowering

- Export typeToTypeExpr from sema and preserve generic type args
- Derive loop variable type from collection element type in sema
- Substitute generic struct type params on field access
- Add getCollectionElementTypeExpr helper in hir_lower
- Replace placeholder collection for-in lowering
- Add Array/Iter lowering: Array_Iter_T / Iter_HasNext_T / Iter_Next_T
- Add Channel lowering: Channel_Recv_Ok_T loop
- Register loop variable in varTypeExprs before body lowering

Fixes _test_forin_stdlib, _test_forin_channel, _test_generic_trait, _test_import, _test_mono
This commit is contained in:
2026-06-14 17:17:56 +03:00
parent 06db4926a7
commit 44bac83471
4 changed files with 712 additions and 7 deletions
+144 -3
View File
@@ -512,6 +512,27 @@ proc getReceiverTypeExpr(ctx: LowerCtx, expr: Expr): TypeExpr =
else: discard
return nil
proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
## Return the element TypeExpr of a collection expression (Array<T>, Iter<T>, Channel<T>).
## For identifiers we can use the declared TypeExpr directly; for other expressions we
## fall back to the resolved concrete Type.
case expr.kind
of ekIdent:
if ctx.varTypeExprs.hasKey(expr.exprIdent):
let te = ctx.varTypeExprs[expr.exprIdent]
if te.kind == tekNamed and te.typeArgs.len > 0:
return te.typeArgs[0]
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
return te.pointerPointee.typeArgs[0]
else:
discard
let t = ctx.resolveExprType(expr)
if t.kind == tkNamed and t.inner.len > 0:
return typeToTypeExpr(t.inner[0])
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
return typeToTypeExpr(t.inner[0].inner[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 =
@@ -1336,10 +1357,130 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
let forBlock = hirBlock(@[initStmt, initStore, whileNode], nil, makeVoid(), loc, isScope = true)
return ctx.flushPending(forBlock)
# Generic iterator for loop (simplified - just infinite loop for now)
let loweredIter = ctx.lowerExpr(iterExpr)
# Collection-based for: for x in collection { body }
let collType = ctx.resolveExprType(iterExpr)
let elemTypeExpr = ctx.getCollectionElementTypeExpr(iterExpr)
let elemType = ctx.resolveTypeExpr(elemTypeExpr)
# Resolve the collection type to its mangled struct instance (e.g. Array<int> -> Array_int).
let collTypeMangled = substituteType(ctx, typeToTypeExpr(collType), ctx.typeSubst)
let isChannel = collType.kind == tkNamed and collType.name.startsWith("Channel")
if isChannel:
# Channel lowering:
# alloca x
# while (true) {
# if (!Channel_Recv_Ok_T(&ch, &x)) break;
# body
# }
let recvOkName = ctx.generateMethodInstance("Channel_Recv_Ok", @[elemTypeExpr])
let xAlloca = hirAlloca(varName, elemType, loc)
let xVar = hirVar(varName, elemType, loc)
ctx.varTypeExprs[varName] = elemTypeExpr
let chAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: ctx.lowerExpr(iterExpr),
typ: makePointer(collType), loc: loc)
let xAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: xVar,
typ: makePointer(elemType), loc: loc)
let recvOkCall = hirCall(recvOkName, @[chAddr, xAddr], makeBool(), loc)
let notRecvOk = HirNode(kind: hUnary, unaryOp: tkBang, unaryOperand: recvOkCall,
typ: makeBool(), loc: loc)
let breakNode = HirNode(kind: hBreak, loc: loc)
let ifNode = HirNode(kind: hIf, ifCond: notRecvOk, ifThen: breakNode, ifElse: nil,
typ: makeVoid(), loc: loc)
let loweredBody = ctx.lowerBlock(body)
return ctx.flushPending(HirNode(kind: hLoop, loopBody: loweredBody, typ: makeVoid(), loc: loc))
var whileBodyStmts: seq[HirNode] = @[]
whileBodyStmts.add(xAlloca)
whileBodyStmts.add(ifNode)
if loweredBody != nil:
whileBodyStmts.add(loweredBody)
let whileBody = hirBlock(whileBodyStmts, nil, makeVoid(), loc)
let trueLit = hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc)
let whileNode = HirNode(kind: hWhile, whileCond: trueLit, whileBody: whileBody,
typ: makeVoid(), loc: loc)
let forBlock = hirBlock(@[whileNode], nil, makeVoid(), loc, isScope = true)
return ctx.flushPending(forBlock)
# Array / Iter lowering:
# alloca __iter
# __iter = Array_Iter_T(&collection);
# while (Iter_HasNext_T(&__iter)) {
# alloca x
# x = Iter_Next_T(&__iter);
# body
# }
let iterFuncName = ctx.generateMethodInstance("Array_Iter", @[elemTypeExpr])
let hasNextFuncName = ctx.generateMethodInstance("Iter_HasNext", @[elemTypeExpr])
let nextFuncName = ctx.generateMethodInstance("Iter_Next", @[elemTypeExpr])
# Ensure Iter<T> struct instance exists and resolve its mangled name.
let iterType = substituteType(ctx, TypeExpr(kind: tekNamed, typeName: "Iter", typeArgs: @[elemTypeExpr]), ctx.typeSubst)
let iterVarName = "__iter_" & varName & "_" & $ctx.varCounter
inc ctx.varCounter
# Build collection pointer. If the collection is not a simple identifier, spill to a temp.
var preStmts: seq[HirNode] = @[]
var collPtr: HirNode = nil
if iterExpr.kind == ekIdent:
let collVar = hirVar(iterExpr.exprIdent, collType, loc)
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: collVar,
typ: makePointer(collType), loc: loc)
else:
let collAllocaName = ctx.freshName()
let collAlloca = hirAlloca(collAllocaName, collTypeMangled, loc)
let collVarPtr = hirVar(collAllocaName, makePointer(collTypeMangled), loc)
let collValue = ctx.lowerExpr(iterExpr)
let collStore = hirStore(collVarPtr, collValue, loc)
preStmts.add(collAlloca)
preStmts.add(collStore)
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp,
unaryOperand: hirVar(collAllocaName, collTypeMangled, loc),
typ: makePointer(collTypeMangled), loc: loc)
let iterAlloca = hirAlloca(iterVarName, iterType, loc)
let iterVarPtr = hirVar(iterVarName, makePointer(iterType), loc)
let iterInitCall = hirCall(iterFuncName, @[collPtr], iterType, loc)
let iterStore = hirStore(iterVarPtr, iterInitCall, loc)
preStmts.add(iterAlloca)
preStmts.add(iterStore)
# while condition: Iter_HasNext_T(&__iter)
let iterAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
typ: makePointer(iterType), loc: loc)
let condCall = hirCall(hasNextFuncName, @[iterAddr], makeBool(), loc)
# loop body: alloca x; x = Iter_Next_T(&__iter); body
let xAlloca = hirAlloca(varName, elemType, loc)
let xVarPtr = hirVar(varName, makePointer(elemType), loc)
let iterAddr2 = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
typ: makePointer(iterType), loc: loc)
let nextCall = hirCall(nextFuncName, @[iterAddr2], elemType, loc)
let xStore = hirStore(xVarPtr, nextCall, loc)
ctx.varTypeExprs[varName] = elemTypeExpr
let loweredBody = ctx.lowerBlock(body)
var bodyStmts: seq[HirNode] = @[]
bodyStmts.add(xAlloca)
bodyStmts.add(xStore)
if loweredBody != nil:
bodyStmts.add(loweredBody)
let whileBody = hirBlock(bodyStmts, nil, makeVoid(), loc)
let whileNode = HirNode(kind: hWhile, whileCond: condCall, whileBody: whileBody,
typ: makeVoid(), loc: loc)
var blockStmts = preStmts
blockStmts.add(whileNode)
let forBlock = hirBlock(blockStmts, nil, makeVoid(), loc, isScope = true)
return ctx.flushPending(forBlock)
of skDoWhile:
let body = ctx.lowerBlock(stmt.stmtDoWhileBody)
+18 -4
View File
@@ -137,7 +137,7 @@ proc typeExprReferencesTypeParam(te: TypeExpr, name: string): bool =
of tekSelf:
return false
proc typeToTypeExpr(t: Type): TypeExpr =
proc typeToTypeExpr*(t: Type): TypeExpr =
## Convert a resolved Type back to a TypeExpr for storage in inferred type args.
case t.kind
of tkInt: TypeExpr(kind: tekNamed, typeName: "int")
@@ -154,7 +154,11 @@ proc typeToTypeExpr(t: Type): TypeExpr =
of tkFloat64: TypeExpr(kind: tekNamed, typeName: "float64")
of tkBool: TypeExpr(kind: tekNamed, typeName: "bool")
of tkStr: TypeExpr(kind: tekNamed, typeName: "String")
of tkNamed: TypeExpr(kind: tekNamed, typeName: t.name)
of tkNamed:
var args: seq[TypeExpr] = @[]
for a in t.inner:
args.add(typeToTypeExpr(a))
return TypeExpr(kind: tekNamed, typeName: t.name, typeArgs: args)
of tkPointer:
if t.inner.len > 0:
TypeExpr(kind: tekPointer, refLifetime: "", pointerPointee: typeToTypeExpr(t.inner[0]))
@@ -201,6 +205,8 @@ proc substituteTypeInType(sema: var Sema, t: Type, subst: Table[string, Type]):
inner.add(sema.substituteTypeInType(it, subst))
return Type(kind: tkFunc, inner: inner)
of tkNamed:
if subst.hasKey(t.name):
return subst[t.name]
if t.inner.len > 0:
var args: seq[Type] = @[]
for a in t.inner:
@@ -1263,9 +1269,13 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
let sym = sema.globalScope.lookup(objType.name)
if sym != nil and sym.decl != nil:
if sym.decl.kind == dkStruct:
var subst = initTable[string, Type]()
for i, tp in sym.decl.declStructTypeParams:
if i < objType.inner.len:
subst[tp.name] = objType.inner[i]
for f in sym.decl.declStructFields:
if f.name == expr.exprFieldName:
return sema.resolveType(f.ftype)
return sema.substituteTypeInType(sema.resolveType(f.ftype), subst)
sema.emitError(expr.loc, &"struct '{objType.name}' has no field '{expr.exprFieldName}'")
elif sym.decl.kind == dkEnum:
# Algebraic enum fields
@@ -1498,11 +1508,15 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
return makeVoid()
of skFor:
let iterExpr = stmt.stmtForIter
discard sema.checkExpr(iterExpr, scope)
let collType = sema.checkExpr(iterExpr, scope)
var forScope = newScope(scope)
var iterTyp = makeUnknown()
if iterExpr.kind == ekRange:
iterTyp = sema.checkExpr(iterExpr.exprRangeLo, scope)
elif collType.kind == tkNamed and collType.inner.len > 0:
iterTyp = collType.inner[0]
elif collType.isPointer and collType.inner.len > 0 and collType.inner[0].kind == tkNamed and collType.inner[0].inner.len > 0:
iterTyp = collType.inner[0].inner[0]
let iterSym = Symbol(kind: skVar, name: stmt.stmtForVar, typ: iterTyp, isMutable: true)
discard forScope.define(iterSym)
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtForBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtForBody.loc, exprBlock: stmt.stmtForBody)), forScope)
@@ -0,0 +1,378 @@
# Fix `for ... in` Iterator Lowering — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the placeholder collection `for ... in` lowering in the bootstrap compiler with correct Array/Iter and Channel lowerings, fixing `_test_forin_stdlib`, `_test_forin_channel`, `_test_generic_trait`, `_test_import`, and `_test_mono`.
**Architecture:** Determine the concrete element type in sema and register the loop variable with it; in HIR lowering emit explicit alloca/store/while nodes that call monomorphized iterator helpers (`Array_Iter_T`, `Iter_HasNext_T`, `Iter_Next_T`) for arrays and `Channel_Recv_Ok_T` for channels. Also fix generic struct field access so direct field mutations on `Array<int>` work.
**Tech Stack:** Nim (bootstrap compiler), Bux integration tests.
---
## Task 1: Export and Fix `typeToTypeExpr`
**Files:**
- Modify: `bootstrap/sema.nim:140-157`
- [ ] **Step 1.1: Export the helper and preserve type args for named types**
Find:
```nim
proc typeToTypeExpr(t: Type): TypeExpr =
```
Change the signature to `proc typeToTypeExpr*(t: Type): TypeExpr =` and update the `tkNamed` branch:
```nim
of tkNamed:
var args: seq[TypeExpr] = @[]
for a in t.inner:
args.add(typeToTypeExpr(a))
return TypeExpr(kind: tekNamed, typeName: t.name, typeArgs: args)
```
This lets HIR lowering round-trip a resolved concrete `Type` back to a `TypeExpr` that can be mangled into the correct struct instance name (e.g. `Array<int>``Array_int`).
---
## Task 2: Derive Loop-Variable Type in Sema
**Files:**
- Modify: `bootstrap/sema.nim:1503-1515`
- [ ] **Step 2.1: Set the loop variable type from the collection's element type**
Find the `of skFor:` branch and update it so `iterTyp` is the collection element type:
```nim
of skFor:
let iterExpr = stmt.stmtForIter
let collType = sema.checkExpr(iterExpr, scope)
var forScope = newScope(scope)
var iterTyp = makeUnknown()
if iterExpr.kind == ekRange:
iterTyp = sema.checkExpr(iterExpr.exprRangeLo, scope)
elif collType.kind == tkNamed and collType.inner.len > 0:
iterTyp = collType.inner[0]
elif collType.isPointer and collType.inner.len > 0 and collType.inner[0].kind == tkNamed and collType.inner[0].inner.len > 0:
iterTyp = collType.inner[0].inner[0]
let iterSym = Symbol(kind: skVar, name: stmt.stmtForVar, typ: iterTyp, isMutable: true)
discard forScope.define(iterSym)
discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtForBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtForBody.loc, exprBlock: stmt.stmtForBody)), forScope)
return makeVoid()
```
---
## Task 3: Substitute Generic Struct Type Parameters on Field Access
**Files:**
- Modify: `bootstrap/sema.nim:1269-1273` and `bootstrap/sema.nim:180-215`
- [ ] **Step 3.1: Build substitution map in `ekField` for struct fields**
In the `dkStruct` branch of `ekField`, build a substitution map from the object's concrete type arguments before resolving the field type:
```nim
if sym.decl.kind == dkStruct:
var subst = initTable[string, Type]()
for i, tp in sym.decl.declStructTypeParams:
if i < objType.inner.len:
subst[tp.name] = objType.inner[i]
for f in sym.decl.declStructFields:
if f.name == expr.exprFieldName:
return sema.substituteTypeInType(sema.resolveType(f.ftype), subst)
sema.emitError(expr.loc, &"struct '{objType.name}' has no field '{expr.exprFieldName}'")
```
- [ ] **Step 3.2: Make `substituteTypeInType` handle named type-parameter names**
In `substituteTypeInType`, add a lookup for `tkNamed` names that are type-parameter names:
```nim
of tkNamed:
if subst.hasKey(t.name):
return subst[t.name]
if t.inner.len > 0:
var args: seq[Type] = @[]
for a in t.inner:
args.add(sema.substituteTypeInType(a, subst))
return Type(kind: tkNamed, name: t.name, inner: args)
return t
```
---
## Task 4: Add `getCollectionElementTypeExpr` Helper in `hir_lower.nim`
**Files:**
- Modify: `bootstrap/hir_lower.nim` (after `resolveExprType` definition)
- [ ] **Step 4.1: Add the helper**
```nim
proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
## Return the element TypeExpr of a collection expression (Array<T>, Iter<T>, Channel<T>).
## For identifiers we can use the declared TypeExpr directly; for other expressions we
## fall back to the resolved concrete Type.
case expr.kind
of ekIdent:
if ctx.varTypeExprs.hasKey(expr.exprIdent):
let te = ctx.varTypeExprs[expr.exprIdent]
if te.kind == tekNamed and te.typeArgs.len > 0:
return te.typeArgs[0]
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
return te.pointerPointee.typeArgs[0]
else:
discard
let t = ctx.resolveExprType(expr)
if t.kind == tkNamed and t.inner.len > 0:
return typeToTypeExpr(t.inner[0])
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
return typeToTypeExpr(t.inner[0].inner[0])
return TypeExpr(kind: tekNamed, typeName: "unknown")
```
---
## Task 5: Implement Collection `for ... in` Lowering
**Files:**
- Modify: `bootstrap/hir_lower.nim` (replace the placeholder at lines 1339-1342)
- [ ] **Step 5.1: Replace the placeholder collection lowering**
Find:
```nim
# Generic iterator for loop (simplified - just infinite loop for now)
let loweredIter = ctx.lowerExpr(iterExpr)
let loweredBody = ctx.lowerBlock(body)
return ctx.flushPending(HirNode(kind: hLoop, loopBody: loweredBody, typ: makeVoid(), loc: loc))
```
Replace with:
```nim
# Collection-based for: for x in collection { body }
let collType = ctx.resolveExprType(iterExpr)
let elemTypeExpr = ctx.getCollectionElementTypeExpr(iterExpr)
let elemType = ctx.resolveTypeExpr(elemTypeExpr)
# Resolve the collection type to its mangled struct instance (e.g. Array<int> -> Array_int).
let collTypeMangled = substituteType(ctx, typeToTypeExpr(collType), ctx.typeSubst)
let isChannel = collType.kind == tkNamed and collType.name.startsWith("Channel")
if isChannel:
# Channel lowering:
# alloca x
# while (true) {
# if (!Channel_Recv_Ok_T(&ch, &x)) break;
# body
# }
let recvOkName = ctx.generateMethodInstance("Channel_Recv_Ok", @[elemTypeExpr])
let xAlloca = hirAlloca(varName, elemType, loc)
let xVar = hirVar(varName, elemType, loc)
ctx.varTypeExprs[varName] = elemTypeExpr
let chAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: ctx.lowerExpr(iterExpr),
typ: makePointer(collTypeMangled), loc: loc)
let xAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: xVar,
typ: makePointer(elemType), loc: loc)
let recvOkCall = hirCall(recvOkName, @[chAddr, xAddr], makeBool(), loc)
let notRecvOk = HirNode(kind: hUnary, unaryOp: tkBang, unaryOperand: recvOkCall,
typ: makeBool(), loc: loc)
let breakNode = HirNode(kind: hBreak, loc: loc)
let ifNode = HirNode(kind: hIf, ifCond: notRecvOk, ifThen: breakNode, ifElse: nil,
typ: makeVoid(), loc: loc)
let loweredBody = ctx.lowerBlock(body)
var whileBodyStmts: seq[HirNode] = @[]
whileBodyStmts.add(xAlloca)
whileBodyStmts.add(ifNode)
if loweredBody != nil:
whileBodyStmts.add(loweredBody)
let whileBody = hirBlock(whileBodyStmts, nil, makeVoid(), loc)
let trueLit = hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc)
let whileNode = HirNode(kind: hWhile, whileCond: trueLit, whileBody: whileBody,
typ: makeVoid(), loc: loc)
let forBlock = hirBlock(@[whileNode], nil, makeVoid(), loc, isScope = true)
return ctx.flushPending(forBlock)
# Array / Iter lowering:
# alloca __iter
# __iter = Array_Iter_T(&collection);
# while (Iter_HasNext_T(&__iter)) {
# alloca x
# x = Iter_Next_T(&__iter);
# body
# }
let iterFuncName = ctx.generateMethodInstance("Array_Iter", @[elemTypeExpr])
let hasNextFuncName = ctx.generateMethodInstance("Iter_HasNext", @[elemTypeExpr])
let nextFuncName = ctx.generateMethodInstance("Iter_Next", @[elemTypeExpr])
# Ensure Iter<T> struct instance exists and resolve its mangled name.
let iterType = substituteType(ctx, TypeExpr(kind: tekNamed, typeName: "Iter", typeArgs: @[elemTypeExpr]), ctx.typeSubst)
let iterVarName = "__iter_" & varName & "_" & $ctx.varCounter
inc ctx.varCounter
# Build collection pointer. If the collection is not a simple identifier, spill to a temp.
var preStmts: seq[HirNode] = @[]
var collPtr: HirNode = nil
if iterExpr.kind == ekIdent:
let collVar = hirVar(iterExpr.exprIdent, collTypeMangled, loc)
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: collVar,
typ: makePointer(collTypeMangled), loc: loc)
else:
let collAllocaName = ctx.freshName()
let collAlloca = hirAlloca(collAllocaName, collTypeMangled, loc)
let collVarPtr = hirVar(collAllocaName, makePointer(collTypeMangled), loc)
let collValue = ctx.lowerExpr(iterExpr)
let collStore = hirStore(collVarPtr, collValue, loc)
preStmts.add(collAlloca)
preStmts.add(collStore)
collPtr = HirNode(kind: hUnary, unaryOp: tkAmp,
unaryOperand: hirVar(collAllocaName, collTypeMangled, loc),
typ: makePointer(collTypeMangled), loc: loc)
let iterAlloca = hirAlloca(iterVarName, iterType, loc)
let iterVarPtr = hirVar(iterVarName, makePointer(iterType), loc)
let iterInitCall = hirCall(iterFuncName, @[collPtr], iterType, loc)
let iterStore = hirStore(iterVarPtr, iterInitCall, loc)
preStmts.add(iterAlloca)
preStmts.add(iterStore)
# while condition: Iter_HasNext_T(&__iter)
let iterAddr = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
typ: makePointer(iterType), loc: loc)
let condCall = hirCall(hasNextFuncName, @[iterAddr], makeBool(), loc)
# loop body: alloca x; x = Iter_Next_T(&__iter); body
let xAlloca = hirAlloca(varName, elemType, loc)
let xVarPtr = hirVar(varName, makePointer(elemType), loc)
let iterAddr2 = HirNode(kind: hUnary, unaryOp: tkAmp, unaryOperand: hirVar(iterVarName, iterType, loc),
typ: makePointer(iterType), loc: loc)
let nextCall = hirCall(nextFuncName, @[iterAddr2], elemType, loc)
let xStore = hirStore(xVarPtr, nextCall, loc)
ctx.varTypeExprs[varName] = elemTypeExpr
let loweredBody = ctx.lowerBlock(body)
var bodyStmts: seq[HirNode] = @[]
bodyStmts.add(xAlloca)
bodyStmts.add(xStore)
if loweredBody != nil:
bodyStmts.add(loweredBody)
let whileBody = hirBlock(bodyStmts, nil, makeVoid(), loc)
let whileNode = HirNode(kind: hWhile, whileCond: condCall, whileBody: whileBody,
typ: makeVoid(), loc: loc)
var blockStmts = preStmts
blockStmts.add(whileNode)
let forBlock = hirBlock(blockStmts, nil, makeVoid(), loc, isScope = true)
return ctx.flushPending(forBlock)
```
---
## Task 6: Build and Verify
**Files:**
- Test: `_test_forin_stdlib`, `_test_forin_channel`, `_test_generic_trait`, `_test_import`, `_test_mono`
- [ ] **Step 6.1: Rebuild the bootstrap compiler**
Run:
```bash
cd /home/ziko/z-git/bux/bux
make build
```
Expected: build succeeds.
- [ ] **Step 6.2: Run the five target integration tests**
Run:
```bash
cd /home/ziko/z-git/bux/bux/_test_forin_stdlib && /home/ziko/z-git/bux/bux/buxc run
cd /home/ziko/z-git/bux/bux/_test_forin_channel && /home/ziko/z-git/bux/bux/buxc run
cd /home/ziko/z-git/bux/bux/_test_generic_trait && /home/ziko/z-git/bux/bux/buxc run
cd /home/ziko/z-git/bux/bux/_test_import && /home/ziko/z-git/bux/bux/buxc run
cd /home/ziko/z-git/bux/bux/_test_mono && /home/ziko/z-git/bux/bux/buxc run
```
Expected: all compile and run; programs that return the sum (`_test_generic_trait`, `_test_import`, `_test_mono`) exit with code 60, which is their expected return value.
- [ ] **Step 6.3: Run `make test`**
```bash
cd /home/ziko/z-git/bux/bux
make test
```
Expected: no new failures.
- [ ] **Step 6.4: Run `make selfhost-loop`**
```bash
cd /home/ziko/z-git/bux/bux
make selfhost-loop
```
Expected: C output and stripped ELF binary remain identical.
---
## Task 7: Commit
- [ ] **Step 7.1: Commit the changes**
```bash
cd /home/ziko/z-git/bux/bux
git add bootstrap/sema.nim bootstrap/hir_lower.nim
git commit -m "fix(bootstrap): implement collection for-in lowering
- Export typeToTypeExpr from sema and preserve generic type args
- Derive loop variable type from collection element type in sema
- Substitute generic struct type params on field access
- Add getCollectionElementTypeExpr helper in hir_lower
- Replace placeholder collection for-in lowering
- Add Array/Iter lowering: Array_Iter_T / Iter_HasNext_T / Iter_Next_T
- Add Channel lowering: Channel_Recv_Ok_T loop
- Register loop variable in varTypeExprs before body lowering
Fixes _test_forin_stdlib, _test_forin_channel, _test_generic_trait, _test_import, _test_mono"
```
---
## Spec Coverage Check
| Spec Requirement | Plan Task |
|---|---|
| Export and fix `typeToTypeExpr` | Task 1 |
| Derive loop variable type in sema | Task 2 |
| Substitute generic struct type params on field access | Task 3 |
| Add `getCollectionElementTypeExpr` helper | Task 4 |
| Array/Iter collection lowering | Task 5 |
| Channel collection lowering | Task 5 |
| Loop variable declaration and scope registration | Task 5 |
| Target tests pass | Task 6 |
| No regressions | Task 6 |
## Placeholder Scan
- No TBD/TODO/fill-in-later steps.
- Every code block contains the exact code to insert.
- Every command contains the exact path and expected outcome.
@@ -0,0 +1,172 @@
# Fix `for ... in` Iterator Lowering in Bootstrap
> **Date:** 2026-06-14
> **Status:** Approved
> **Scope:** Bootstrap compiler (`bootstrap/hir_lower.nim`) — `for <var> in <collection>` loops
## 1. Problem Statement
The `for ... in` collection iterator is already parsed and sema-accepted, but the HIR→C lowering produces broken code for concrete generic collections such as `Array<int>` or `Channel<int>`.
Five integration tests fail because of this:
- `_test_forin_stdlib`: C compilation error `x undeclared` inside `Main`.
- `_test_forin_channel`: C compilation error `msg undeclared` inside `Main`.
- `_test_generic_trait`: sema error `cannot assign float32 to int at 12:9` — the loop variable `x` is typed as `float32` instead of `int`.
- `_test_import`: sema error `cannot assign float32 to int at 15:9` — same issue.
- `_test_mono`: sema error `cannot assign float32 to int at 38:9` — same issue.
Two independent bugs are at play:
1. **Missing C declaration:** the loop variable is not emitted as a local variable in the generated C function.
2. **Wrong loop-variable type:** generic `Iter_Next_T` returns an unsubstituted type parameter `T`, which sema treats as `float32` by default.
## 2. Goals
1. Make `_test_forin_stdlib` pass.
2. Make `_test_forin_channel` pass.
3. Make `_test_generic_trait`, `_test_import`, and `_test_mono` pass (or at least no longer fail with the `float32` loop-variable error).
4. Keep the fix minimal and confined to the bootstrap compiler.
5. Ensure `make test` and `make selfhost-loop` continue to pass.
## 3. Non-Goals
1. Rewriting the `apps/` example applications (excluded per user request).
2. Adding new language features such as `break`/`continue` in loops or iterator traits.
3. Changing the selfhost compiler (`src/*.bux`) unless required for selfhost-loop parity.
4. Fixing the unrelated `_test_slice` C typedef bug (tracked separately).
5. Implementing full Destructors / Drop trait (next roadmap milestone after stabilization).
## 4. Background: How `for ... in` Is Lowered
The parser produces `stmtForIn(ident, expr, body)`.
Sema checks the collection expression and infers the loop-variable type from it.
HIR lowering currently desugars the loop to a placeholder infinite `hLoop` that ignores the iterator expression and never declares the loop variable.
The intended lowering depends on the collection type:
- **Array / Iter collections** (from `lib/Iter.bux`):
```bux
let __iter = Array_Iter_T(&collection);
while (Iter_HasNext_T(&__iter)) {
let x = Iter_Next_T(&__iter);
// body
}
```
- **Channel collections** (from `lib/Channel.bux`):
```bux
var x: T;
while (true) {
if (!Channel_Recv_Ok_T(&ch, &x)) { break; }
// body
}
```
## 5. Investigation Plan
1. Read `_test_forin_stdlib/src/Main.bux`, `_test_forin_channel/src/Main.bux`, `_test_generic_trait/src/Main.bux`.
2. Locate `stmtForIn` handling in `bootstrap/hir_lower.nim`.
3. Verify how the iterator variable is introduced into HIR/C:
- Is it registered in `ctx.varTypes` / `ctx.varTypeExprs`?
- Is a `hirLet` emitted before the loop body?
4. Verify how `Array_Iter_Next_T` is monomorphized:
- Does the call get the `_int` suffix?
- Does the returned `T` get substituted with `int`?
5. Identify the minimal edit that fixes both symptoms.
## 6. Hypothesis
### 6.1 Missing Declaration
The lowering creates the loop body with references to the loop variable, but the declaration site is omitted or not added to the function's local-variable list. As a result, the C backend never emits `int x;`, causing the `undeclared identifier` error.
### 6.2 Wrong Type
When the collection is `Array<int>`, the lowering calls `Array_Iter_Next` (without `_int`) or calls a monomorphized `Array_Iter_Next_int` but assigns its result to a variable whose type was resolved from the generic signature (`T`). Because sema does not substitute `T` for the loop variable, the variable defaults to `float32`.
The fix is likely to:
- Explicitly declare the loop variable with the concrete element type derived from the collection type.
- Ensure `Array_Iter` / `Array_Iter_HasNext` / `Array_Iter_Next` are monomorphized with the collection's type arguments.
## 7. Proposed Fix
The fix spans sema and HIR lowering:
1. **Sema fixes**:
- In `skFor`, derive the loop-variable type from the collection's element type (`Array<T>` / `Channel<T>` inner type).
- In `ekField` for structs, build a type-parameter substitution map from the object's concrete type args and apply it to the field type, so `arr.data` on `Array<int>` resolves to `*int`.
- Fix `typeToTypeExpr` to preserve generic type arguments on named types.
2. **HIR lowering**:
- Replace the placeholder `skFor` collection branch with real lowering that selects by collection type.
- **Array / Iter collections**:
- Extract element type from `Array<T>` or `Iter<T>`.
- Generate or reuse `Iter_T` struct instance.
- Generate function instances `Array_Iter_T`, `Iter_HasNext_T`, `Iter_Next_T`.
- If the collection expression is not a simple identifier, spill it to a temporary `Array_T` variable first.
- Emit `alloca __iter`, store `Array_Iter_T(&collection)`, then `while (Iter_HasNext_T(&__iter)) { let x = Iter_Next_T(&__iter); body }`.
- Register `x` in `ctx.varTypeExprs` before lowering the body.
- **Channel collections**:
- Extract element type from `Channel<T>`.
- Generate function instance `Channel_Recv_Ok_T`.
- Emit `alloca x`, then `while (true) { if (!Channel_Recv_Ok_T(&ch, &x)) break; body }`.
## 8. Affected Files
| File | Expected Change |
|------|-----------------|
| `bootstrap/sema.nim` | Derive loop var type; substitute struct type params on field access; preserve type args in `typeToTypeExpr`. |
| `bootstrap/hir_lower.nim` | Replace placeholder collection `skFor` lowering with Array/Iter and Channel lowerings. |
| `_test_forin_stdlib/src/Main.bux` | No changes. |
| `_test_forin_channel/src/Main.bux` | No changes. |
| `_test_generic_trait/src/Main.bux` | No changes. |
| `_test_import/src/Main.bux` | No changes. |
| `_test_mono/src/Main.bux` | No changes. |
## 9. Testing Plan
### 9.1 Target Tests
```bash
cd _test_forin_stdlib && /home/ziko/z-git/bux/bux/buxc run
cd _test_forin_channel && /home/ziko/z-git/bux/bux/buxc run
cd _test_generic_trait && /home/ziko/z-git/bux/bux/buxc run
cd _test_import && /home/ziko/z-git/bux/bux/buxc run
cd _test_mono && /home/ziko/z-git/bux/bux/buxc run
```
Expected: all exit 0.
### 9.2 Regression Tests
```bash
cd /home/ziko/z-git/bux/bux
make test
make selfhost-loop
```
Expected: all pass / no changes in selfhost-loop output.
## 10. Success Criteria
- `_test_forin_stdlib` reports PASS.
- `_test_forin_channel` reports PASS.
- `_test_generic_trait`, `_test_import`, `_test_mono` no longer fail with the `float32` loop-variable error.
- `make test` reports no new failures.
- `make selfhost-loop` remains deterministic.
## 11. Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Fix changes loop lowering and breaks range-based `for i in 0..10` | Keep range-based path separate; only touch `stmtForIn` collection path. |
| Same bug exists in selfhost compiler | Selfhost-loop will catch output differences; fix selfhost only if required. |
| Root cause is deeper than loop lowering | Time-box investigation; report findings if not resolved quickly. |
## 12. Relation to Other Work
- Previous commit `06db492` fixed generic `operator []` resolution. This work continues generic-type propagation into iterator loops.
- `_test_slice` has a separate C typedef bug and is out of scope for this task.