feat: LIR backend replaces direct HIR→C emission (v0.3.0)

- 42 LIR instructions in bootstrap/lir.nim

- HIR→LIR lowering in bootstrap/lir_lower.nim

- LIR→C emission with full struct/enum/vtable support

- All 22 examples + 5 Nim unit tests passing

- Updated README status and backend description
This commit is contained in:
2026-06-06 18:55:59 +03:00
parent aff03ffb5a
commit 62a9c8b84a
6 changed files with 1507 additions and 5 deletions
+625
View File
@@ -0,0 +1,625 @@
## LIR Lowering — HIR → LIR
## Converts the high-level HIR tree into flat, linear LIR instructions.
## Each HIR node kind lowers to 1-20 LIR instructions.
import std/[strutils, strformat, tables, sequtils]
import ast, types, token, hir, lir
type
LowerToLirCtx* = object
builder*: LirBuilder
## Map HIR var names -> C type names (for alloca/load/store type info)
varTypes*: Table[string, string]
## Map HIR var names -> LirValue kind (lvkVar or lvkTemp)
varLirValues*: Table[string, LirValue]
## C types for function params / returns
funcRetType*: string
## Current source location for debug
currentFile*: string
proc initLowerToLirCtx*(): LowerToLirCtx =
result = LowerToLirCtx(
builder: initLirBuilder(),
varTypes: initTable[string, string](),
varLirValues: initTable[string, LirValue](),
)
# ── Helpers ──
proc cEscape(s: string): string =
result = ""
for c in s:
case c
of '\\': result.add("\\\\")
of '"': result.add("\\\"")
of '\n': result.add("\\n")
of '\r': result.add("\\r")
of '\t': result.add("\\t")
of '\0': result.add("\\0")
else: result.add(c)
proc typeToCStr(typ: Type): string =
## Convert a Bux Type to a C type string.
if typ == nil: return "int"
case typ.kind
of tkVoid: return "void"
of tkBool, tkBool8, tkBool16, tkBool32: return "bool"
of tkChar8: return "char"
of tkChar16: return "char16_t"
of tkChar32: return "char32_t"
of tkStr: return "const char*"
of tkInt8: return "int8_t"
of tkInt16: return "int16_t"
of tkInt32: return "int32_t"
of tkInt64: return "int64_t"
of tkInt: return "int"
of tkUInt8: return "uint8_t"
of tkUInt16: return "uint16_t"
of tkUInt32: return "uint32_t"
of tkUInt64: return "uint64_t"
of tkUInt: return "unsigned int"
of tkFloat32: return "float"
of tkFloat64: return "double"
of tkPointer, tkRef, tkMutRef:
if typ.inner.len > 0:
return typeToCStr(typ.inner[0]) & "*"
return "void*"
of tkDynRef:
return typ.name & "_FatPtr"
of tkSlice:
let elem = if typ.inner.len > 0: typeToCStr(typ.inner[0]) else: "void"
return "Slice_" & elem.replace(" ", "_").replace("*", "Ptr")
of tkNamed:
case typ.name
of "String", "str": return "const char*"
of "int": return "int"
of "int8": return "int8_t"
of "int16": return "int16_t"
of "int32": return "int32_t"
of "int64": return "int64_t"
of "uint": return "unsigned int"
of "uint8": return "uint8_t"
of "uint16": return "uint16_t"
of "uint32": return "uint32_t"
of "uint64": return "uint64_t"
of "float32": return "float"
of "float64": return "double"
of "bool": return "bool"
else: return typ.name
else: return "int"
proc hirTypeToC(ctx: var LowerToLirCtx, node: HirNode): string =
if node == nil: return "int"
result = typeToCStr(node.typ)
proc binOpToLir(op: TokenKind): LirKind =
case op
of tkPlus: lirAdd
of tkMinus: lirSub
of tkStar: lirMul
of tkSlash: lirDiv
of tkPercent: lirMod
of tkAmp: lirAnd
of tkPipe: lirOr
of tkCaret: lirXor
of tkShl: lirShl
of tkShr: lirShr
else: lirAdd
proc cmpOpToLir(op: TokenKind): LirKind =
case op
of tkEq: lirCmpEq
of tkNe: lirCmpNe
of tkLt: lirCmpLt
of tkLe: lirCmpLe
of tkGt: lirCmpGt
of tkGe: lirCmpGe
else: lirCmpEq
# ── Forward declarations ──
proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue
proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode)
# ── Lowering: Expressions → LirValue ──
proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
if node == nil: return lirInt(0)
template b: var LirBuilder = ctx.builder
case node.kind
# ── Literals ──
of hLit:
case node.litToken.kind
of tkBoolLiteral:
if node.litToken.text == "true": return lirInt(1)
else: return lirInt(0)
of tkStringLiteral:
var text = node.litToken.text
# Handle backtick strings
if text.len >= 2 and text[0] == '`' and text[text.len-1] == '`':
text = "\"" & cEscape(text[1 ..< text.len-1]) & "\""
elif text.len >= 2 and text[0] == '"' and text[text.len-1] == '"':
# Strip c8" c16" c32" prefixes
if text.startsWith("c32\""):
text = "\"" & cEscape(text[4 ..< text.len-1]) & "\""
elif text.startsWith("c16\""):
text = "\"" & cEscape(text[4 ..< text.len-1]) & "\""
elif text.startsWith("c8\""):
text = "\"" & cEscape(text[3 ..< text.len-1]) & "\""
else:
text = "\"" & cEscape(text[1 ..< text.len-1]) & "\""
elif text.len >= 2 and text[0] == '"':
text = "\"" & cEscape(text[1 ..< text.len]) & "\""
else:
text = "\"" & cEscape(text) & "\""
return lirStr(text)
of tkNull:
return lirInt(0)
else:
# Integer/float literal
return lirVar(node.litToken.text)
# ── Variable reference ──
of hVar:
let name = node.varName
if ctx.varLirValues.hasKey(name):
return ctx.varLirValues[name]
return lirVar(name)
# ── Self ──
of hSelf:
return lirVar("self")
# ── Unary ──
of hUnary:
let operand = lowerExpr(ctx, node.unaryOperand)
case node.unaryOp
of tkMinus:
let t = b.freshTemp()
b.emitUnary(lirNeg, t, operand)
return t
of tkBang:
let t = b.freshTemp()
b.emitUnary(lirNot, t, operand)
return t
of tkTilde:
let t = b.freshTemp()
b.emitUnary(lirBNot, t, operand)
return t
of tkStar:
# Dereference: *ptr → load
let t = b.freshTemp()
b.emitLoad(t, operand)
return t
of tkAmp:
# Address of: &var
let t = b.freshTemp()
b.emitAddrOf(t, operand)
return t
else:
return operand
# ── Binary ──
of hBinary:
let left = lowerExpr(ctx, node.binaryLeft)
let right = lowerExpr(ctx, node.binaryRight)
case node.binaryOp
of tkEq, tkNe, tkLt, tkLe, tkGt, tkGe:
let t = b.freshTemp()
b.emitCmp(cmpOpToLir(node.binaryOp), t, left, right)
return t
of tkAmpAmp, tkPipePipe:
# Logical and/or: lowered to select
let t = b.freshTemp()
if node.binaryOp == tkAmpAmp:
# left && right → left ? (right != 0) : 0
let rhsBool = b.freshTemp()
b.emitCmp(lirCmpNe, rhsBool, right, lirInt(0))
b.emitSelect(t, left, rhsBool, lirInt(0))
else:
# left || right → left ? 1 : (right != 0)
let rhsBool = b.freshTemp()
b.emitCmp(lirCmpNe, rhsBool, right, lirInt(0))
b.emitSelect(t, left, lirInt(1), rhsBool)
return t
else:
let t = b.freshTemp()
b.emitBinOp(binOpToLir(node.binaryOp), t, left, right)
return t
# ── Call ──
of hCall:
var args: seq[LirValue] = @[]
for arg in node.callArgs:
args.add(lowerExpr(ctx, arg))
let callee = node.callCallee
let t = b.freshTemp()
let cType = hirTypeToC(ctx, node)
if cType != "void" and cType != "":
b.emitAlloca(t.strVal, cType)
b.emitCall(t, callee, args)
return t
# ── CallIndirect ──
of hCallIndirect:
let callee = lowerExpr(ctx, node.callIndirectCallee)
var args: seq[LirValue] = @[callee]
for arg in node.callIndirectArgs:
args.add(lowerExpr(ctx, arg))
let t = b.freshTemp()
let cType = hirTypeToC(ctx, node)
if cType != "void" and cType != "":
b.emitAlloca(t.strVal, cType)
# Use lirCallIndirect: dst = (*fn_ptr)(args...)
b.emit(LirInstr(kind: lirCallIndirect, dst: t, src: callee, extra: args[1..^1]))
return t
# ── Field pointer expressions (return address) ──
# These return a typed pointer (void* for now, cast before deref)
of hFieldPtr:
let base = lowerExpr(ctx, node.fieldPtrBase)
let baseTyp = node.fieldPtrBase.typ
let isPtr = baseTyp != nil and baseTyp.kind in {tkPointer, tkRef, tkMutRef}
let t = b.freshTemp()
b.emitAlloca(t.strVal, "void*")
if isPtr:
b.emitRawC(&"{t.strVal} = (void*)&({base.strVal}->{node.fieldName});")
else:
b.emitRawC(&"{t.strVal} = (void*)&({base.strVal}.{node.fieldName});")
return t
of hArrowField:
let base = lowerExpr(ctx, node.arrowFieldBase)
let t = b.freshTemp()
b.emitAlloca(t.strVal, "void*")
b.emitRawC(&"{t.strVal} = (void*)&({base.strVal}->{node.arrowFieldName});")
return t
of hIndexPtr:
let base = lowerExpr(ctx, node.indexPtrBase)
let idx = lowerExpr(ctx, node.indexPtrIndex)
let t = b.freshTemp()
b.emitAlloca(t.strVal, "void*")
b.emitRawC(&"{t.strVal} = (void*)&({base.strVal}[{idx.strVal}]);")
return t
# ── Load ──
of hLoad:
# Load through a pointer or field access
# Optimize common patterns: load(field_ptr) → direct field access
if node.loadPtr != nil and node.loadPtr.kind == hArrowField:
let base = lowerExpr(ctx, node.loadPtr.arrowFieldBase)
let cType = hirTypeToC(ctx, node)
let t = b.freshTemp()
b.emitAlloca(t.strVal, cType)
b.emitRawC(&"{t.strVal} = {base.strVal}->{node.loadPtr.arrowFieldName};")
return t
if node.loadPtr != nil and node.loadPtr.kind == hFieldPtr:
let base = lowerExpr(ctx, node.loadPtr.fieldPtrBase)
let baseTyp = node.loadPtr.fieldPtrBase.typ
let isPtr = baseTyp != nil and baseTyp.kind in {tkPointer, tkRef, tkMutRef}
let cType = hirTypeToC(ctx, node)
let t = b.freshTemp()
b.emitAlloca(t.strVal, cType)
if isPtr:
b.emitRawC(&"{t.strVal} = {base.strVal}->{node.loadPtr.fieldName};")
else:
b.emitRawC(&"{t.strVal} = {base.strVal}.{node.loadPtr.fieldName};")
return t
if node.loadPtr != nil and node.loadPtr.kind == hIndexPtr:
let base = lowerExpr(ctx, node.loadPtr.indexPtrBase)
let idx = lowerExpr(ctx, node.loadPtr.indexPtrIndex)
let cType = hirTypeToC(ctx, node)
let t = b.freshTemp()
b.emitAlloca(t.strVal, cType)
b.emitRawC(&"{t.strVal} = {base.strVal}[{idx.strVal}];")
return t
# Generic: dereference pointer
let ptrVal = lowerExpr(ctx, node.loadPtr)
let cType = hirTypeToC(ctx, node)
let t = b.freshTemp()
b.emitAlloca(t.strVal, cType)
b.emitRawC(&"{t.strVal} = *({cType}*){ptrVal.strVal};")
return t
# ── Slice Index ──
of hSliceIndex:
let base = lowerExpr(ctx, node.sliceIndexBase)
let idx = lowerExpr(ctx, node.sliceIndexIndex)
let t = b.freshTemp()
# Emit: base.data[idx] (with optional bounds check)
if node.sliceIndexBoundsCheck:
b.emitRawC(&"bux_bounds_check((size_t)({idx.strVal}), ({base.strVal}).len)")
b.emit(LirInstr(kind: lirLoad, dst: t, src: base, src2: idx))
return t
# ── Cast ──
of hCast:
let operand = lowerExpr(ctx, node.castOperand)
let targetCType = typeToCStr(node.castType)
let t = b.freshTemp()
b.emitCast(t, operand, targetCType)
return t
# ── SizeOf ──
of hSizeOf:
let ctype = typeToCStr(node.sizeOfType)
let t = b.freshTemp()
b.emit(LirInstr(kind: lirRawC, src: lirStr(&"/* sizeof({ctype}) */")))
return lirVar(&"sizeof({ctype})")
# ── Spawn ──
of hSpawn:
if node.spawnAsync:
let t = b.freshTemp()
b.emitAlloca(t.strVal, "void*")
b.emitCall(t, "bux_async_spawn", @[lirGlobal(node.spawnCallee)])
return t
else:
var args: seq[LirValue] = @[]
if node.spawnArgs.len > 0:
args.add(lowerExpr(ctx, node.spawnArgs[0]))
let t = b.freshTemp()
b.emitAlloca(t.strVal, "void*")
b.emitCall(t, "bux_task_spawn", @[lirGlobal(node.spawnCallee)] & args)
return t
# ── DynRef (trait object) ──
of hDynRef:
let data = lowerExpr(ctx, node.dynRefData)
let t = b.freshTemp()
let fatPtrType = node.dynRefInterface & "_FatPtr"
b.emitRawC(&"{fatPtrType} {t.strVal};")
b.emitMov(lirVar(t.strVal & ".data"), data)
b.emitMov(lirVar(t.strVal & ".vtable"), lirGlobal(node.dynRefConcreteType & "_" & node.dynRefInterface & "_VTable"))
return t
# ── DynCall ──
of hDynCall:
let receiver = lowerExpr(ctx, node.dynCallReceiver)
var args: seq[LirValue] = @[receiver]
for i in 1 ..< node.dynCallArgs.len:
args.add(lowerExpr(ctx, node.dynCallArgs[i]))
let t = b.freshTemp()
b.emitRawC(&"{t.strVal} = {receiver.strVal}.vtable->{node.dynCallMethod}({args.mapIt($it).join(\", \")});")
return t
# ── StructInit ──
of hStructInit:
var fields: seq[tuple[name: string, val: LirValue]] = @[]
for f in node.structInitFields:
fields.add((f.name, lowerExpr(ctx, f.value)))
let t = b.freshTemp()
b.emitStructInit(t, node.structInitName, fields)
return t
# ── SliceInit ──
of hSliceInit:
let t = b.freshTemp()
let elemType = if node.typ.inner.len > 0: typeToCStr(node.typ.inner[0]) else: "void"
var elems: seq[LirValue] = @[]
for e in node.sliceInitElements:
elems.add(lowerExpr(ctx, e))
# Create a temporary array, then wrap in slice
let arrTmp = b.freshTemp()
b.emitRawC(&"{elemType} {arrTmp.strVal}[] = {{{elems.mapIt($it).join(\", \")}}};")
b.emitSliceInit(t, elemType, arrTmp, lirInt(node.sliceInitLen))
return t
# ── TupleInit ──
of hTupleInit:
var elems: seq[LirValue] = @[]
for e in node.tupleInitElements:
elems.add(lowerExpr(ctx, e))
let t = b.freshTemp()
b.emitRawC(&"/* tuple */ {t.strVal} = {{{elems.mapIt($it).join(\", \")}}};")
return t
# ── If expression (ternary) ──
of hIf:
if node.ifThen.kind != hBlock and node.ifElse != nil:
# Simple ternary
let cond = lowerExpr(ctx, node.ifCond)
let thenVal = lowerExpr(ctx, node.ifThen)
let elseVal = lowerExpr(ctx, node.ifElse)
let t = b.freshTemp()
b.emitSelect(t, cond, thenVal, elseVal)
return t
else:
# Complex if — fallback to block lowering
# This shouldn't happen if lowering is done right, but handle gracefully
return lirInt(0)
# ── Block expression (returns last expr) ──
of hBlock:
for stmt in node.blockStmts:
lowerStmt(ctx, stmt)
if node.blockExpr != nil:
return lowerExpr(ctx, node.blockExpr)
return lirVoid()
# ── Match (lowered by hir_lower already, but handle if present) ──
of hMatch:
# Should have been lowered by hir_lower.nim already
return lirInt(0)
else:
# Fallback for unhandled expression kinds
b.emitComment(&"/* unhandled expr kind: {node.kind} */")
return lirInt(0)
# ── Lowering: Statements → void ──
proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
if node == nil: return
template b: var LirBuilder = ctx.builder
case node.kind
# ── Return ──
of hReturn:
if node.returnValue != nil:
let val = lowerExpr(ctx, node.returnValue)
b.emitRet(val)
else:
b.emitRet()
# ── If statement ──
of hIf:
# Lower to: cond = lower(ifCond); jz else_label, cond
# lower(ifThen); jmp end_label
# else_label: lower(ifElse); end_label:
let cond = lowerExpr(ctx, node.ifCond)
let elseLbl = b.freshLabel("else")
let endLbl = b.freshLabel("endif")
if node.ifElse != nil:
b.emitJz(elseLbl, cond)
lowerStmt(ctx, node.ifThen)
b.emitJmp(endLbl)
b.emitLabel(elseLbl)
lowerStmt(ctx, node.ifElse)
b.emitLabel(endLbl)
else:
b.emitJz(endLbl, cond)
lowerStmt(ctx, node.ifThen)
b.emitLabel(endLbl)
# ── While statement ──
of hWhile:
let startLbl = b.freshLabel("while")
let bodyLbl = b.freshLabel("wbody")
let endLbl = b.freshLabel("wend")
b.emitLabel(startLbl)
let cond = lowerExpr(ctx, node.whileCond)
b.emitJz(endLbl, cond)
lowerStmt(ctx, node.whileBody)
b.emitJmp(startLbl)
b.emitLabel(endLbl)
# ── Loop (infinite) ──
of hLoop:
let startLbl = b.freshLabel("loop")
b.emitLabel(startLbl)
lowerStmt(ctx, node.loopBody)
b.emitJmp(startLbl)
# ── Break ──
of hBreak:
# We emit a break label reference; the emitter tracks the nearest loop end
b.emitRawC("break;")
# ── Continue ──
of hContinue:
b.emitRawC("continue;")
# ── Alloca ──
of hAlloca:
let cType = typeToCStr(node.allocaType)
let name = node.allocaName
ctx.varTypes[name] = cType
ctx.varLirValues[name] = lirVar(name)
b.emitAlloca(name, cType)
# ── Store ──
of hStore:
# If storing to a simple variable, use mov (direct assignment)
if node.storePtr.kind == hVar:
let val = lowerExpr(ctx, node.storeValue)
b.emitMov(lirVar(node.storePtr.varName), val)
else:
let ptrVal = lowerExpr(ctx, node.storePtr)
let val = lowerExpr(ctx, node.storeValue)
# ptrVal is a void* address; cast and store
let valCType = hirTypeToC(ctx, node.storeValue)
b.emitRawC(&"*({valCType}*){ptrVal.strVal} = {val.strVal};")
# ── Assign ──
of hAssign:
let target = lowerExpr(ctx, node.assignTarget)
let value = lowerExpr(ctx, node.assignValue)
case node.assignOp
of tkAssign:
b.emitMov(target, value)
of tkPlusAssign:
let t = b.freshTemp()
b.emitBinOp(lirAdd, t, target, value)
b.emit(LirInstr(kind: lirStore, src: t, src2: target))
of tkMinusAssign:
let t = b.freshTemp()
b.emitBinOp(lirSub, t, target, value)
b.emit(LirInstr(kind: lirStore, src: t, src2: target))
else:
b.emitMov(target, value)
# ── Call statement (void return) ──
of hCall:
var args: seq[LirValue] = @[]
for arg in node.callArgs:
args.add(lowerExpr(ctx, arg))
b.emitCallVoid(node.callCallee, args)
# ── CallIndirect statement ──
of hCallIndirect:
let callee = lowerExpr(ctx, node.callIndirectCallee)
var args: seq[LirValue] = @[]
for arg in node.callIndirectArgs:
args.add(lowerExpr(ctx, arg))
b.emit(LirInstr(kind: lirCallIndirect, src: callee, extra: args))
# ── Block ──
of hBlock:
if node.isScope:
b.emitRawC("{")
for stmt in node.blockStmts:
lowerStmt(ctx, stmt)
if node.blockExpr != nil:
let exprVal = lowerExpr(ctx, node.blockExpr)
# If block is an expression, store result
discard
if node.isScope:
b.emitRawC("}")
# ── Emit (inline C) ──
of hEmit:
b.emitRawC(node.emitCode)
# ── Expression statement ──
else:
let exprVal = lowerExpr(ctx, node)
# Expression evaluated for side effects; temp is unused
discard
# ── Module-level lowering ──
proc lowerModuleToLir*(hirMod: HirModule): LirBuilder =
## Convert a full HIR module into LIR functions.
var ctx = initLowerToLirCtx()
for f in hirMod.funcs:
var params: seq[tuple[name: string, cType: string]] = @[]
for p in f.params:
let ct = typeToCStr(p.typ)
params.add((p.name, ct))
ctx.varTypes[p.name] = ct
ctx.varLirValues[p.name] = lirVar(p.name)
let retCT = if f.retType != nil: typeToCStr(f.retType) else: "void"
ctx.funcRetType = retCT
ctx.builder.beginFunc(f.name, params, retCT, f.isPublic)
if f.body != nil:
if f.body.kind == hBlock:
for stmt in f.body.blockStmts:
lowerStmt(ctx, stmt)
if f.body.blockExpr != nil and f.retType != nil and f.retType.kind != tkVoid:
let val = lowerExpr(ctx, f.body.blockExpr)
ctx.builder.emitRet(val)
else:
lowerStmt(ctx, f.body)
ctx.builder.endFunc()
return ctx.builder