feat: add HIR, C backend, and end-to-end compilation

- Phase 3: High-Level IR (HIR) with lowering from AST
  - Method call desugaring (obj.method() → Type_method(obj))
  - if/else, while, loop, break, continue lowering
  - struct, enum, function lowering
  - 8 HIR tests passing

- Phase 5A: C backend code generation
  - Type mapping (Bux types → C11 types)
  - Expression and statement emission
  - Struct, enum, function generation
  - C main() wrapper for Bux Main()

- Runtime shim (stdlib/runtime.c)
  - bux_alloc, bux_free, bux_print, bux_panic
  - BuxString, BuxSlice types
  - Bounds checking, division by zero

- Build integration
  - bux build: lex → parse → sema → HIR → C → cc
  - bux run: build + execute
  - bux clean: remove build directory

- Parser fixes
  - Newline handling in struct, enum, extend, interface blocks
  - self keyword as expression and parameter name

- Sema improvements
  - Method resolution (extend blocks)
  - Interface conformance checking
  - collectGlobals made public

- All 70 tests passing (25 lexer + 16 parser + 21 sema + 8 HIR)
- End-to-end: Bux programs compile to native ELF64 binaries
This commit is contained in:
2026-05-30 22:40:34 +03:00
parent 8e637c89e7
commit 8e74215378
15 changed files with 2074 additions and 78 deletions
+464
View File
@@ -0,0 +1,464 @@
import std/[tables, strformat, strutils]
import ast, types, token, source_location, hir, sema, scope
type
LowerCtx* = object
module*: Module
globalScope*: Scope
methodTable*: Table[string, seq[MethodInfo]]
currentFuncRetType*: Type
varCounter*: int
proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter
result = "__tmp_" & $ctx.varCounter
proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.module = module
result.globalScope = sema.globalScope
result.methodTable = sema.methodTable
result.varCounter = 0
# Forward declarations
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode
proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
if expr == nil: return makeUnknown()
case expr.kind
of ekLiteral:
case expr.exprLit.kind
of tkIntLiteral: return makeInt()
of tkFloatLiteral: return makeFloat64()
of tkStringLiteral: return makeStr()
of tkCharLiteral: return makeChar8()
of tkBoolLiteral: return makeBool()
else: return makeUnknown()
of ekIdent:
let sym = ctx.globalScope.lookup(expr.exprIdent)
if sym != nil and sym.typ != nil: return sym.typ
return makeUnknown()
of ekSelf: return makeNamed("self")
of ekBinary:
let left = ctx.resolveExprType(expr.exprBinaryLeft)
case expr.exprBinaryOp
of tkEq, tkNe, tkLt, tkLe, tkGt, tkGe, tkAmpAmp, tkPipePipe:
return makeBool()
else: return left
of ekUnary:
case expr.exprUnaryOp
of tkBang: return makeBool()
of tkAmp: return makePointer(ctx.resolveExprType(expr.exprUnaryOperand))
of tkStar:
let inner = ctx.resolveExprType(expr.exprUnaryOperand)
if inner.isPointer: return inner.inner[0]
return makeUnknown()
else: return ctx.resolveExprType(expr.exprUnaryOperand)
of ekCall:
if expr.exprCallCallee.kind == ekIdent:
let sym = ctx.globalScope.lookup(expr.exprCallCallee.exprIdent)
if sym != nil and sym.typ != nil and sym.typ.kind == tkFunc:
return sym.typ.inner[^1]
if expr.exprCallCallee.kind == ekField:
let recvType = ctx.resolveExprType(expr.exprCallCallee.exprFieldObj)
let methodName = expr.exprCallCallee.exprFieldName
var typeName = ""
if recvType.kind == tkNamed: typeName = recvType.name
elif recvType.isPointer and recvType.inner.len > 0 and recvType.inner[0].kind == tkNamed:
typeName = recvType.inner[0].name
if typeName != "" and ctx.methodTable.hasKey(typeName):
for minfo in ctx.methodTable[typeName]:
if minfo.name == methodName:
return minfo.retType
return makeUnknown()
of ekField:
let objType = ctx.resolveExprType(expr.exprFieldObj)
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 tekPointer: return makePointer(makeUnknown())
else: return makeUnknown()
return makeUnknown()
of ekStructInit: return makeNamed(expr.exprStructInitName)
of ekSlice:
if expr.exprSliceElements.len > 0:
return makeSlice(ctx.resolveExprType(expr.exprSliceElements[0]))
return makeSlice(makeUnknown())
of ekTuple:
var elems: seq[Type] = @[]
for e in expr.exprTupleElements:
elems.add(ctx.resolveExprType(e))
return makeTuple(elems)
of ekCast:
if expr.exprCastType != nil:
case expr.exprCastType.kind
of tekNamed: return makeNamed(expr.exprCastType.typeName)
else: return makeUnknown()
return makeUnknown()
of ekBlock:
if expr.exprBlock.stmts.len > 0:
let last = expr.exprBlock.stmts[^1]
if last.kind == skExpr:
return ctx.resolveExprType(last.stmtExpr)
return makeVoid()
else: return makeUnknown()
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
if expr == nil: return nil
let loc = expr.loc
let typ = ctx.resolveExprType(expr)
case expr.kind
of ekLiteral:
return hirLit(expr.exprLit, typ, loc)
of ekIdent:
return hirVar(expr.exprIdent, typ, loc)
of ekSelf:
return hirSelf(typ, loc)
of ekUnary:
let operand = ctx.lowerExpr(expr.exprUnaryOperand)
return hirUnary(expr.exprUnaryOp, operand, typ, loc)
of ekBinary:
let left = ctx.lowerExpr(expr.exprBinaryLeft)
let right = ctx.lowerExpr(expr.exprBinaryRight)
return hirBinary(expr.exprBinaryOp, left, right, typ, loc)
of ekCall:
# Method call desugaring: obj.method(args) → Type_method(obj, args)
if expr.exprCallCallee.kind == ekField:
let recvType = ctx.resolveExprType(expr.exprCallCallee.exprFieldObj)
let methodName = expr.exprCallCallee.exprFieldName
var typeName = ""
if recvType.kind == tkNamed: typeName = recvType.name
elif recvType.isPointer and recvType.inner.len > 0 and recvType.inner[0].kind == tkNamed:
typeName = recvType.inner[0].name
if typeName != "" and ctx.methodTable.hasKey(typeName):
for minfo in ctx.methodTable[typeName]:
if minfo.name == methodName:
# Desugar: obj.method(args) → Type_method(&obj, args)
let mangledName = typeName & "_" & methodName
var args: seq[HirNode] = @[]
args.add(ctx.lowerExpr(expr.exprCallCallee.exprFieldObj))
for arg in expr.exprCallArgs:
args.add(ctx.lowerExpr(arg))
return hirCall(mangledName, args, typ, loc)
# Regular function call
var calleeName = ""
if expr.exprCallCallee.kind == ekIdent:
calleeName = expr.exprCallCallee.exprIdent
elif expr.exprCallCallee.kind == ekPath:
calleeName = expr.exprCallCallee.exprPath.join("::")
var args: seq[HirNode] = @[]
for arg in expr.exprCallArgs:
args.add(ctx.lowerExpr(arg))
if calleeName != "":
return hirCall(calleeName, args, typ, loc)
else:
let callee = ctx.lowerExpr(expr.exprCallCallee)
return HirNode(kind: hCallIndirect, callIndirectCallee: callee,
callIndirectArgs: args, typ: typ, loc: loc)
of ekField:
let base = ctx.lowerExpr(expr.exprFieldObj)
let basePtr = HirNode(kind: hFieldPtr, fieldPtrBase: base,
fieldName: expr.exprFieldName,
typ: makePointer(typ), loc: loc)
return HirNode(kind: hLoad, loadPtr: basePtr, typ: typ, loc: loc)
of ekIndex:
let base = ctx.lowerExpr(expr.exprIndexObj)
let idx = ctx.lowerExpr(expr.exprIndexIdx)
let basePtr = HirNode(kind: hIndexPtr, indexPtrBase: base,
indexPtrIndex: idx, typ: makePointer(typ), loc: loc)
return HirNode(kind: hLoad, loadPtr: basePtr, typ: typ, loc: loc)
of ekAssign:
let target = ctx.lowerExpr(expr.exprAssignTarget)
let value = ctx.lowerExpr(expr.exprAssignValue)
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
assignTarget: target, assignValue: value,
typ: makeVoid(), loc: loc)
of ekStructInit:
var fields: seq[tuple[name: string, value: HirNode]] = @[]
for f in expr.exprStructInitFields:
fields.add((f.name, ctx.lowerExpr(f.value)))
return HirNode(kind: hStructInit, structInitName: expr.exprStructInitName,
structInitFields: fields, typ: typ, loc: loc)
of ekSlice:
var elems: seq[HirNode] = @[]
for e in expr.exprSliceElements:
elems.add(ctx.lowerExpr(e))
return HirNode(kind: hSliceInit, sliceInitElements: elems, typ: typ, loc: loc)
of ekTuple:
var elems: seq[HirNode] = @[]
for e in expr.exprTupleElements:
elems.add(ctx.lowerExpr(e))
return HirNode(kind: hTupleInit, tupleInitElements: elems, typ: typ, loc: loc)
of ekCast:
let operand = ctx.lowerExpr(expr.exprCastOperand)
var castType = makeUnknown()
if expr.exprCastType != nil:
case expr.exprCastType.kind
of tekNamed: castType = makeNamed(expr.exprCastType.typeName)
of tekPointer: castType = makePointer(makeUnknown())
else: discard
return HirNode(kind: hCast, castOperand: operand, castType: castType,
typ: typ, loc: loc)
of ekBlock:
return ctx.lowerBlock(expr.exprBlock)
of ekPostfix:
let operand = ctx.lowerExpr(expr.exprPostfixOperand)
return HirNode(kind: hUnary, unaryOp: expr.exprPostfixOp,
unaryOperand: operand, typ: typ, loc: loc)
of ekTernary:
let cond = ctx.lowerExpr(expr.exprTernaryCond)
let thenE = ctx.lowerExpr(expr.exprTernaryThen)
let elseE = ctx.lowerExpr(expr.exprTernaryElse)
return HirNode(kind: hIf, ifCond: cond, ifThen: thenE, ifElse: elseE,
typ: typ, loc: loc)
of ekIs:
let operand = ctx.lowerExpr(expr.exprIsOperand)
var isType = makeUnknown()
if expr.exprIsType != nil and expr.exprIsType.kind == tekNamed:
isType = makeNamed(expr.exprIsType.typeName)
return HirNode(kind: hIs, isOperand: operand, isType: isType,
typ: makeBool(), loc: loc)
of ekMatch:
let subject = ctx.lowerExpr(expr.exprMatchSubject)
var arms: seq[HirMatchArm] = @[]
for arm in expr.exprMatchArms:
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body)))
return HirNode(kind: hMatch, matchSubject: subject, matchArms: arms,
typ: typ, loc: loc)
of ekSizeOf:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeInt(), loc: loc)
of ekIntrinsic:
return HirNode(kind: hLit, litToken: Token(kind: tkStringLiteral, text: "\"\"", loc: loc),
typ: makeStr(), loc: loc)
else:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if stmt == nil: return nil
let loc = stmt.loc
case stmt.kind
of skExpr:
return ctx.lowerExpr(stmt.stmtExpr)
of skLet:
let initHir = ctx.lowerExpr(stmt.stmtLetInit)
let allocaType = if stmt.stmtLetType != nil:
case stmt.stmtLetType.kind
of tekNamed:
case stmt.stmtLetType.typeName
of "int", "int32": makeInt()
of "int64": makeInt64()
of "float64": makeFloat64()
of "float32": makeFloat32()
of "bool": makeBool()
else: makeNamed(stmt.stmtLetType.typeName)
of tekPointer: makePointer(makeUnknown())
else: makeUnknown()
else:
ctx.resolveExprType(stmt.stmtLetInit)
let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc)
let varNode = hirVar(stmt.stmtLetName, makePointer(allocaType), loc)
let store = hirStore(varNode, initHir, loc)
return HirNode(kind: hBlock, blockStmts: @[alloca, store],
blockExpr: nil, typ: makeVoid(), loc: loc)
of skReturn:
let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil
return hirReturn(value, loc)
of skIf:
let cond = ctx.lowerExpr(stmt.stmtIfCond)
let thenBlock = ctx.lowerBlock(stmt.stmtIfThen)
var elseBlock: HirNode = nil
if stmt.stmtIfElse != nil:
elseBlock = ctx.lowerBlock(stmt.stmtIfElse)
elif stmt.stmtIfElseIfs.len > 0:
# Desugar else-if chain
var current: HirNode = nil
for i in countdown(stmt.stmtIfElseIfs.len - 1, 0):
let elifBranch = stmt.stmtIfElseIfs[i]
let elifCond = ctx.lowerExpr(elifBranch.cond)
let elifBlock = ctx.lowerBlock(elifBranch.blk)
current = HirNode(kind: hIf, ifCond: elifCond, ifThen: elifBlock,
ifElse: current, typ: makeVoid(), loc: elifBranch.loc)
elseBlock = current
return HirNode(kind: hIf, ifCond: cond, ifThen: thenBlock, ifElse: elseBlock,
typ: makeVoid(), loc: loc)
of skWhile:
let cond = ctx.lowerExpr(stmt.stmtWhileCond)
let body = ctx.lowerBlock(stmt.stmtWhileBody)
return HirNode(kind: hWhile, whileCond: cond, whileBody: body,
typ: makeVoid(), loc: loc)
of skLoop:
let body = ctx.lowerBlock(stmt.stmtLoopBody)
return HirNode(kind: hLoop, loopBody: body, typ: makeVoid(), loc: loc)
of skBreak:
return HirNode(kind: hBreak, breakLabel: stmt.stmtBreakLabel,
typ: makeVoid(), loc: loc)
of skContinue:
return HirNode(kind: hContinue, continueLabel: stmt.stmtContinueLabel,
typ: makeVoid(), loc: loc)
of skFor:
# Desugar: for i in iter { body } → { let __iter = iter; while __hasNext(__iter) { let i = __next(__iter); body } }
let iterExpr = ctx.lowerExpr(stmt.stmtForIter)
let body = ctx.lowerBlock(stmt.stmtForBody)
# Simplified: just lower the body for now
return HirNode(kind: hLoop, loopBody: body, typ: makeVoid(), loc: loc)
of skDoWhile:
let body = ctx.lowerBlock(stmt.stmtDoWhileBody)
let cond = ctx.lowerExpr(stmt.stmtDoWhileCond)
let whileNode = HirNode(kind: hWhile, whileCond: cond, whileBody: body,
typ: makeVoid(), loc: loc)
return HirNode(kind: hBlock, blockStmts: @[body, whileNode],
blockExpr: nil, typ: makeVoid(), loc: loc)
of skMatch:
let subject = ctx.lowerExpr(stmt.stmtMatchSubject)
var arms: seq[HirMatchArm] = @[]
for arm in stmt.stmtMatchArms:
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body)))
return HirNode(kind: hMatch, matchSubject: subject, matchArms: arms,
typ: makeVoid(), loc: loc)
of skDecl:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode =
if blk == nil: return nil
var stmts: seq[HirNode] = @[]
for s in blk.stmts:
let hir = ctx.lowerStmt(s)
if hir != nil:
stmts.add(hir)
return HirNode(kind: hBlock, blockStmts: stmts, blockExpr: nil,
typ: makeVoid(), loc: blk.loc)
proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
var params: seq[tuple[name: string, typ: Type]] = @[]
for p in decl.declFuncParams:
var pType = makeUnknown()
if p.ptype != nil:
case p.ptype.kind
of tekNamed:
case p.ptype.typeName
of "int", "int32": pType = makeInt()
of "int64": pType = makeInt64()
of "float64": pType = makeFloat64()
of "float32": pType = makeFloat32()
of "bool": pType = makeBool()
of "Point", "Self": pType = makeNamed(p.ptype.typeName)
else: pType = makeNamed(p.ptype.typeName)
of tekPointer: pType = makePointer(makeUnknown())
else: discard
params.add((p.name, pType))
var retType = makeVoid()
if decl.declFuncReturnType != nil:
case decl.declFuncReturnType.kind
of tekNamed:
case decl.declFuncReturnType.typeName
of "int", "int32": retType = makeInt()
of "int64": retType = makeInt64()
of "float64": retType = makeFloat64()
of "float32": retType = makeFloat32()
of "bool": retType = makeBool()
else: retType = makeNamed(decl.declFuncReturnType.typeName)
of tekPointer: retType = makePointer(makeUnknown())
else: discard
ctx.currentFuncRetType = retType
let body = if decl.declFuncBody != nil: ctx.lowerBlock(decl.declFuncBody) else: nil
result = HirFunc(name: decl.declFuncName, params: params, retType: retType,
body: body, isPublic: decl.isPublic)
proc lowerModule*(module: Module, sema: Sema): HirModule =
var ctx = initLowerCtx(module, sema)
var funcs: seq[HirFunc] = @[]
var structs: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]] = @[]
var enums: seq[tuple[name: string, variants: seq[string]]] = @[]
var consts: seq[tuple[name: string, typ: Type, value: HirNode]] = @[]
for decl in module.items:
case decl.kind
of dkFunc:
funcs.add(ctx.lowerFunc(decl))
of dkImpl:
for methodDecl in decl.declImplMethods:
if methodDecl.kind == dkFunc:
var hf = ctx.lowerFunc(methodDecl)
hf.name = decl.declImplTypeName & "_" & hf.name
funcs.add(hf)
of dkStruct:
var fields: seq[tuple[name: string, typ: Type]] = @[]
for f in decl.declStructFields:
var fType = makeUnknown()
if f.ftype != nil and f.ftype.kind == tekNamed:
case f.ftype.typeName
of "float64": fType = makeFloat64()
of "float32": fType = makeFloat32()
of "int", "int32": fType = makeInt()
else: fType = makeNamed(f.ftype.typeName)
fields.add((f.name, fType))
structs.add((decl.declStructName, fields))
of dkEnum:
var variants: seq[string] = @[]
for v in decl.declEnumVariants:
variants.add(v.name)
enums.add((decl.declEnumName, variants))
of dkConst:
let value = ctx.lowerExpr(decl.declConstValue)
let typ = if decl.declConstType != nil:
case decl.declConstType.kind
of tekNamed: makeNamed(decl.declConstType.typeName)
else: makeUnknown()
else: makeUnknown()
consts.add((decl.declConstName, typ, value))
else: discard
result = HirModule(funcs: funcs, structs: structs, enums: enums, consts: consts)