From 44bac83471736b2d7583015699d2a7041b6e060a Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sun, 14 Jun 2026 17:17:56 +0300 Subject: [PATCH] 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 --- bootstrap/hir_lower.nim | 147 ++++++- bootstrap/sema.nim | 22 +- ...-06-14-forin-iterator-lowering-fix-plan.md | 378 ++++++++++++++++++ ...6-14-forin-iterator-lowering-fix-design.md | 172 ++++++++ 4 files changed, 712 insertions(+), 7 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-14-forin-iterator-lowering-fix-plan.md create mode 100644 docs/superpowers/specs/2026-06-14-forin-iterator-lowering-fix-design.md diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 836d3c0..0b05f5e 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -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, Iter, Channel). + ## 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 -> 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) + 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 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) - return ctx.flushPending(HirNode(kind: hLoop, loopBody: loweredBody, typ: makeVoid(), loc: loc)) + + 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) diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 0547cba..bec25c9 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -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) diff --git a/docs/superpowers/plans/2026-06-14-forin-iterator-lowering-fix-plan.md b/docs/superpowers/plans/2026-06-14-forin-iterator-lowering-fix-plan.md new file mode 100644 index 0000000..994e37a --- /dev/null +++ b/docs/superpowers/plans/2026-06-14-forin-iterator-lowering-fix-plan.md @@ -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` 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` → `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, Iter, Channel). + ## 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 -> 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 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. diff --git a/docs/superpowers/specs/2026-06-14-forin-iterator-lowering-fix-design.md b/docs/superpowers/specs/2026-06-14-forin-iterator-lowering-fix-design.md new file mode 100644 index 0000000..60197cb --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-forin-iterator-lowering-fix-design.md @@ -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 in ` 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` or `Channel`. + +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`, 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` / `Channel` 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` 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` or `Iter`. + - 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`. + - 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.