fix(lir): resolve all LIR backend regressions — 26/26 examples + selfhost passing

- hir_lower: add isScope=true to lowerBlock; fix resolveExprType for ekIndex,
  ekField on monomorphized structs, and ekTry/ekUnwrap tmpVar typing
- lir_lower: add loop label stack for break/continue; pass null arg to
  bux_task_spawn when spawn has no arguments
- lir_c_backend: multi-pass type inference for temps; include params in
  varTypes; handle lvkTemp in lirLoad; use inferred type in lirAlloca
This commit is contained in:
2026-06-06 19:49:24 +03:00
parent 4f7653a410
commit 797ca7c80c
3 changed files with 191 additions and 60 deletions
+66 -19
View File
@@ -352,22 +352,63 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
if objType.isPointer and objType.inner.len > 0:
objType = objType.inner[0]
if objType.kind == tkNamed:
let sym = ctx.globalScope.lookup(objType.name)
if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct:
for f in sym.decl.declStructFields:
if f.name == expr.exprFieldName:
if f.ftype != nil:
case f.ftype.kind
of tekNamed:
case f.ftype.typeName
of "int", "int32", "int64": return makeInt()
of "float64": return makeFloat64()
of "float32": return makeFloat32()
of "bool": return makeBool()
else: return makeNamed(f.ftype.typeName)
of tekOwn, tekPointer:
return ctx.resolveTypeExpr(f.ftype)
else: return makeUnknown()
var sym = ctx.globalScope.lookup(objType.name)
var decl = if sym != nil: sym.decl else: nil
# If the type is a monomorphized generic struct instance, look up the base
if decl == nil and ctx.structInstMap.hasKey(objType.name):
let (baseName, typeArgs) = ctx.structInstMap[objType.name]
let baseSym = ctx.globalScope.lookup(baseName)
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkStruct:
decl = baseSym.decl
var subst = initTable[string, Type]()
for i, tp in decl.declStructTypeParams:
if i < typeArgs.len:
subst[tp.name] = typeArgs[i]
for f in decl.declStructFields:
if f.name == expr.exprFieldName:
if f.ftype != nil:
case f.ftype.kind
of tekNamed:
case f.ftype.typeName
of "int", "int32", "int64": return makeInt()
of "float64": return makeFloat64()
of "float32": return makeFloat32()
of "bool": return makeBool()
else:
if subst.hasKey(f.ftype.typeName):
return subst[f.ftype.typeName]
return makeNamed(f.ftype.typeName)
of tekOwn, tekPointer:
return substituteType(ctx, f.ftype, subst)
else: return makeUnknown()
if decl != nil:
case decl.kind
of dkStruct:
for f in decl.declStructFields:
if f.name == expr.exprFieldName:
if f.ftype != nil:
case f.ftype.kind
of tekNamed:
case f.ftype.typeName
of "int", "int32", "int64": return makeInt()
of "float64": return makeFloat64()
of "float32": return makeFloat32()
of "bool": return makeBool()
else: return makeNamed(f.ftype.typeName)
of tekOwn, tekPointer:
return ctx.resolveTypeExpr(f.ftype)
else: return makeUnknown()
of dkEnum:
# Algebraic enum fields: tag and data
if expr.exprFieldName == "tag":
return makeNamed(objType.name & "_Tag")
elif expr.exprFieldName == "data":
return makeNamed(objType.name & "_Data")
else:
# Enum variant field access: e.g., r.data.Ok_0
# We can't easily resolve this here; return unknown
return makeUnknown()
else: discard
return makeUnknown()
of ekStructInit:
if expr.exprStructInitTypeArgs.len > 0:
@@ -395,6 +436,13 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
return makeInt()
of ekUnwrap:
return makeInt()
of ekIndex:
let baseType = ctx.resolveExprType(expr.exprIndexObj)
if baseType.isSlice and baseType.inner.len > 0:
return baseType.inner[0]
if baseType.isPointer and baseType.inner.len > 0:
return baseType.inner[0]
return makeUnknown()
of ekBlock:
if expr.exprBlock.stmts.len > 0:
let last = expr.exprBlock.stmts[^1]
@@ -721,7 +769,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
let tmpName = ctx.freshTryVar()
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
let tmpVar = hirVar(tmpName, makePointer(operandType), loc)
let tmpVar = hirVar(tmpName, operandType, loc)
let tmpStore = hirStore(tmpVar, operand, loc)
let tagPtr = HirNode(kind: hFieldPtr, fieldPtrBase: tmpVar, fieldName: "tag",
@@ -1020,8 +1068,7 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode =
# but for hVar/hLit/hCall etc. we could treat them as block expr.
# For now, leave as-is to avoid breaking control-flow statements.
discard
return HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr,
typ: if expr != nil: expr.typ else: makeVoid(), loc: blk.loc)
return hirBlock(stmts, expr, if expr != nil: expr.typ else: makeVoid(), blk.loc, isScope = true)
proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
# Set up type substitution for generic functions
+99 -38
View File
@@ -178,7 +178,12 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
# ── Alloca ──
of lirAlloca:
be.emitLine(&"{v(instr.src)} {v(instr.dst)};")
var ct = v(instr.src)
if instr.dst.strVal.len > 0 and be.tempTypes.hasKey(instr.dst.strVal):
let inferred = be.tempTypes[instr.dst.strVal]
if inferred != "" and inferred != ct:
ct = inferred
be.emitLine(&"{ct} {v(instr.dst)};")
# ── Pointers ──
of lirAddrOf:
@@ -236,7 +241,7 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
# ── Function emission ──
proc emitFunc(be: var LirCBackend, f: LirFunc) =
proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, string]) =
var paramsStr = ""
for i, p in f.params:
if i > 0: paramsStr.add(", ")
@@ -247,50 +252,99 @@ proc emitFunc(be: var LirCBackend, f: LirFunc) =
be.emitLine(&"{f.retType} {f.name}({paramsStr}) {{")
be.indent += 1
# First pass: declare all temp variables at the top of the function
var tempsSeen: seq[tuple[name: string, cType: string]] = @[]
# ── Pass 1: collect types from allocas, params, and instructions ──
var varTypes = initTable[string, string]()
var tempsSet: seq[string] = @[]
for p in f.params:
varTypes[p.name] = p.cType
be.tempTypes[p.name] = p.cType
for instr in f.instrs:
# Allocas are explicit declarations — mark name as declared, skip emission here
if instr.kind == lirAlloca:
if instr.dst.strVal.len > 0 and instr.dst.strVal notin tempsSet:
if instr.kind == lirAlloca and instr.dst.kind == lvkVar and instr.src.kind == lvkType:
varTypes[instr.dst.strVal] = instr.src.strVal
be.tempTypes[instr.dst.strVal] = instr.src.strVal
if instr.dst.strVal notin tempsSet:
tempsSet.add(instr.dst.strVal)
continue
# Temps that are destinations of binary ops, calls, etc.
if instr.dst.kind == lvkTemp and instr.dst.strVal.len > 0 and instr.dst.strVal notin tempsSet:
# Infer type based on instruction kind
var ct = "int"
# ── Pass 2: iterative type inference for temps ──
var changed = true
while changed:
changed = false
for instr in f.instrs:
if instr.dst.kind != lvkTemp or instr.dst.strVal.len == 0:
continue
let name = instr.dst.strVal
let oldType = if be.tempTypes.hasKey(name): be.tempTypes[name] else: ""
var newType = oldType
case instr.kind
of lirMov, lirLoad, lirLoadGlobal:
ct = "int"
of lirAdd, lirSub, lirMul, lirDiv, lirMod, lirNeg,
lirCmpEq, lirCmpNe, lirCmpLt, lirCmpLe, lirCmpGt, lirCmpGe:
ct = "int"
of lirAnd, lirOr, lirXor, lirShl, lirShr, lirNot, lirBNot:
ct = "int"
of lirCall, lirCallIndirect:
ct = "int" # Conservative
of lirAddrOf, lirFieldPtr, lirArrowFieldPtr, lirIndexPtr, lirPtrAdd:
ct = "void*"
of lirStructInit, lirSliceInit:
ct = "int" # Will be overwritten by the actual type in emit
of lirStructInit:
if instr.extra.len > 0 and instr.extra[0].kind == lvkType:
newType = instr.extra[0].strVal
of lirSliceInit:
if instr.extra.len > 0 and instr.extra[0].kind == lvkType:
newType = "Slice_" & instr.extra[0].strVal
of lirCast:
ct = valToC(be, instr.src2)
if instr.src2.kind == lvkType:
newType = instr.src2.strVal
of lirCall:
if instr.src.kind == lvkGlobal and funcRetTypes.hasKey(instr.src.strVal):
newType = funcRetTypes[instr.src.strVal]
of lirCallIndirect:
# Conservative; try to infer from dst usage in later passes
discard
of lirMov:
if instr.src.kind == lvkTemp and be.tempTypes.hasKey(instr.src.strVal):
newType = be.tempTypes[instr.src.strVal]
elif instr.src.kind == lvkVar and varTypes.hasKey(instr.src.strVal):
newType = varTypes[instr.src.strVal]
of lirLoad, lirLoadGlobal:
# Try to deduce pointee type from pointer vars/temps
if instr.src.kind == lvkVar and varTypes.hasKey(instr.src.strVal):
let srcType = varTypes[instr.src.strVal]
if srcType.endsWith("*"):
newType = srcType[0 ..< srcType.len - 1]
elif srcType.startsWith("Slice_"):
newType = srcType[6 ..< srcType.len]
elif instr.src.kind == lvkTemp and be.tempTypes.hasKey(instr.src.strVal):
let srcType = be.tempTypes[instr.src.strVal]
if srcType.endsWith("*"):
newType = srcType[0 ..< srcType.len - 1]
elif srcType.startsWith("Slice_"):
newType = srcType[6 ..< srcType.len]
of lirSelect:
ct = "int"
if instr.src2.kind == lvkTemp and be.tempTypes.hasKey(instr.src2.strVal):
newType = be.tempTypes[instr.src2.strVal]
elif instr.extra.len > 0 and instr.extra[0].kind == lvkTemp and be.tempTypes.hasKey(instr.extra[0].strVal):
newType = be.tempTypes[instr.extra[0].strVal]
elif instr.src2.kind == lvkVar and varTypes.hasKey(instr.src2.strVal):
newType = varTypes[instr.src2.strVal]
of lirAddrOf, lirFieldPtr, lirArrowFieldPtr, lirIndexPtr, lirPtrAdd:
newType = "void*"
of lirAdd, lirSub, lirMul, lirDiv, lirMod, lirNeg,
lirCmpEq, lirCmpNe, lirCmpLt, lirCmpLe, lirCmpGt, lirCmpGe,
lirAnd, lirOr, lirXor, lirShl, lirShr, lirNot, lirBNot:
newType = "int"
else:
ct = "int"
tempsSeen.add((instr.dst.strVal, ct))
tempsSet.add(instr.dst.strVal)
be.tempTypes[instr.dst.strVal] = ct
discard
# Declare all temps
for t in tempsSeen:
# Skip if the type is a struct type (will be declared inline)
if t.cType == "int" or t.cType == "void*" or t.cType == "double":
be.emitLine(&"{t.cType} {t.name};")
if newType != "" and newType != oldType:
be.tempTypes[name] = newType
changed = true
# Emit instructions in order
# ── Pass 3: declare temps that were inferred ──
var declared: seq[string] = @[]
for instr in f.instrs:
if instr.kind == lirAlloca and instr.dst.strVal.len > 0 and instr.dst.strVal notin declared:
declared.add(instr.dst.strVal)
continue
if instr.dst.kind == lvkTemp and instr.dst.strVal.len > 0 and instr.dst.strVal notin declared:
if be.tempTypes.hasKey(instr.dst.strVal):
let ct = be.tempTypes[instr.dst.strVal]
if ct != "":
declared.add(instr.dst.strVal)
be.emitLine(&"{ct} {instr.dst.strVal};")
# ── Pass 4: emit instructions ──
for instr in f.instrs:
be.emitInstr(instr)
@@ -422,6 +476,13 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
## Emit full C source from LIR builder + HIR module metadata.
be.output = ""
# Build function return type lookup table
var funcRetTypes = initTable[string, string]()
for f in module.funcs:
funcRetTypes[f.name] = typeToCStr(f.retType)
for f in module.externFuncs:
funcRetTypes[f.name] = typeToCStr(f.retType)
# Header
be.emitLine("/* Generated by Bux Compiler (LIR backend) */")
be.emitLine("#include <stdio.h>")
@@ -539,7 +600,7 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
# Emit all LIR functions
for f in builder.funcs:
be.emitFunc(f)
be.emitFunc(f, funcRetTypes)
# C main wrapper
var hasMain = false
+26 -3
View File
@@ -16,12 +16,17 @@ type
funcRetType*: string
## Current source location for debug
currentFile*: string
## Loop end labels for break/continue
loopEndLabels*: seq[string]
loopStartLabels*: seq[string]
proc initLowerToLirCtx*(): LowerToLirCtx =
result = LowerToLirCtx(
builder: initLirBuilder(),
varTypes: initTable[string, string](),
varLirValues: initTable[string, LirValue](),
loopEndLabels: @[],
loopStartLabels: @[],
)
# ── Helpers ──
@@ -360,6 +365,8 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
var args: seq[LirValue] = @[]
if node.spawnArgs.len > 0:
args.add(lowerExpr(ctx, node.spawnArgs[0]))
else:
args.add(lirInt(0))
let t = b.freshTemp()
b.emitAlloca(t.strVal, "void*")
b.emitCall(t, "bux_task_spawn", @[lirGlobal(node.spawnCallee)] & args)
@@ -492,28 +499,44 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
let bodyLbl = b.freshLabel("wbody")
let endLbl = b.freshLabel("wend")
ctx.loopStartLabels.add(startLbl.strVal)
ctx.loopEndLabels.add(endLbl.strVal)
b.emitLabel(startLbl)
let cond = lowerExpr(ctx, node.whileCond)
b.emitJz(endLbl, cond)
lowerStmt(ctx, node.whileBody)
b.emitJmp(startLbl)
b.emitLabel(endLbl)
discard ctx.loopStartLabels.pop()
discard ctx.loopEndLabels.pop()
# ── Loop (infinite) ──
of hLoop:
let startLbl = b.freshLabel("loop")
let endLbl = b.freshLabel("lend")
ctx.loopStartLabels.add(startLbl.strVal)
ctx.loopEndLabels.add(endLbl.strVal)
b.emitLabel(startLbl)
lowerStmt(ctx, node.loopBody)
b.emitJmp(startLbl)
b.emitLabel(endLbl)
discard ctx.loopStartLabels.pop()
discard ctx.loopEndLabels.pop()
# ── Break ──
of hBreak:
# We emit a break label reference; the emitter tracks the nearest loop end
b.emitRawC("break;")
if ctx.loopEndLabels.len > 0:
b.emitJmp(lirLabel(ctx.loopEndLabels[^1]))
else:
b.emitRawC("break;")
# ── Continue ──
of hContinue:
b.emitRawC("continue;")
if ctx.loopStartLabels.len > 0:
b.emitJmp(lirLabel(ctx.loopStartLabels[^1]))
else:
b.emitRawC("continue;")
# ── Alloca ──
of hAlloca: