Phase 7.9: Self-hosted compiler buxc2 builds and runs! 🎉

buxc (Nim bootstrap) successfully compiles buxc2 (Bux self-hosted compiler)
into a working 88KB ELF x86-64 binary.

Compiler fixes (Nim):
- duplicate symbol: user funcs shadow stdlib funcs via mergeDecls()
- forward declarations: func without body + definition (both orderings)
- extern func dedup: same extern in multiple files
- discard keyword: new language keyword, lowered to expr stmt or no-op
- parser: keywords as field names + advance-on-error safeguard
- parser: var without initializer (zero-init)
- parser: multi-line || && continuation expressions
- parser: else-if chain newline handling
- C backend: const declarations emitted as #define
- C backend: load(field_ptr) → base.field optimization (fixes lvalue errors)

Source fixes (src_bux/*.bux):
- types.bux: tk* → ty* prefix for type kind constants (avoid token conflict)
- sema.bux: remaining tk* → ty* references, StringMap → *void workaround
- hir_lower.bux: ekReturn removed, Lcx_LowerParam helper, *f dereference
- c_backend.bux: &mod.funcs[i] for pointer passing
- cli.bux: ReadFile/WriteFile → bux_read_file/bux_write_file wrappers
- parser.bux: pathStr.len → String_Len(pathStr)
- types.bux: "*" + x → String_Concat("*", x)

Build system:
- Makefile: added 4 missing examples + selfhost target
- PLAN.md: updated with actual project state, Phase 7.9 marked complete

All 18 examples pass, all unit tests pass.
This commit is contained in:
2026-05-31 16:34:36 +03:00
parent 166954204c
commit cb256397bd
31 changed files with 4603 additions and 284 deletions
+33
View File
@@ -188,6 +188,19 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
return &"({callee})({argsStr})"
of hLoad:
# Optimize: load(field_ptr(base, field)) → base.field (avoids & on temporaries)
if node.loadPtr != nil and node.loadPtr.kind == hFieldPtr:
let base = be.emitExpr(node.loadPtr.fieldPtrBase)
return &"({base}.{node.loadPtr.fieldName})"
# Optimize: load(arrow_field(base, field)) → base->field
if node.loadPtr != nil and node.loadPtr.kind == hArrowField:
let base = be.emitExpr(node.loadPtr.arrowFieldBase)
return &"({base}->{node.loadPtr.arrowFieldName})"
# Optimize: load(index_ptr(base, idx)) → base[idx]
if node.loadPtr != nil and node.loadPtr.kind == hIndexPtr:
let base = be.emitExpr(node.loadPtr.indexPtrBase)
let idx = be.emitExpr(node.loadPtr.indexPtrIndex)
return &"({base}[{idx}])"
let ptrExpr = be.emitExpr(node.loadPtr)
return &"(*{ptrExpr})"
@@ -482,6 +495,26 @@ proc emitModule*(be: var CBackend, module: HirModule): string =
be.emitExternDecl(ef)
be.emitLine("")
# Const declarations as #define
if module.consts.len > 0:
be.emitLine("/* Constants */")
for c in module.consts:
let val = c.value
if val != nil and val.kind == hLit:
let tok = val.litToken
case tok.kind
of tkIntLiteral:
be.emitLine(&"#define {c.name} {tok.text}")
of tkStringLiteral:
be.emitLine(&"#define {c.name} \"{tok.text}\"")
of tkBoolLiteral:
be.emitLine(&"#define {c.name} {tok.text}")
else:
be.emitLine(&"/* const {c.name} (unsupported literal kind) */")
else:
be.emitLine(&"/* const {c.name} (complex expression) */")
be.emitLine("")
# Struct definitions
for s in module.structs:
be.emitStruct(s.name, s.fields)
+37 -11
View File
@@ -1,4 +1,4 @@
import std/[os, strutils, terminal, strformat, osproc]
import std/[os, strutils, terminal, strformat, osproc, sets]
import lexer, parser, ast, sema, manifest, hir, hir_lower, c_backend, types, scope
type
@@ -142,6 +142,8 @@ Output = "Bin"
return 0
proc collectStdlibDecls(stdlibDir: string): seq[Decl]
proc getDeclName(d: Decl): string
proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl]
proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
@@ -203,13 +205,11 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
stdlibDir = path
break
let stdlibDecls = collectStdlibDecls(stdlibDir)
var mergedItems = stdlibDecls
for decl in allModuleItems:
mergedItems.add(decl)
let mergedItems = mergeDecls(stdlibDecls, allModuleItems)
var unifiedModule = newModule("main")
unifiedModule.items = mergedItems
let semaRes = analyze(unifiedModule)
if semaRes.hasErrors:
printError("type errors in project", useColor)
@@ -239,6 +239,34 @@ proc collectStdlibDecls(stdlibDir: string): seq[Decl] =
else:
result.add(item)
proc getDeclName(d: Decl): string =
case d.kind
of dkFunc: d.declFuncName
of dkExternFunc: d.declExtFuncName
of dkStruct: d.declStructName
of dkEnum: d.declEnumName
of dkUnion: d.declUnionName
of dkInterface: d.declInterfaceName
of dkConst: d.declConstName
of dkTypeAlias: d.declAliasName
else: ""
proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] =
## Merge stdlib and user declarations.
## User funcs shadow stdlib funcs with the same name (simple overload avoidance).
var userNames: HashSet[string]
for d in userDecls:
let name = getDeclName(d)
if name != "":
userNames.incl(name)
result = @[]
for d in stdlibDecls:
let name = getDeclName(d)
if name == "" or name notin userNames:
result.add(d)
for d in userDecls:
result.add(d)
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
@@ -308,14 +336,12 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
return 1
# Phase 2: Merge all project declarations with stdlib
var mergedItems = stdlibDecls
for decl in allModuleItems:
mergedItems.add(decl)
let mergedItems = mergeDecls(stdlibDecls, allModuleItems)
# Create unified module
var unifiedModule = newModule("main")
unifiedModule.items = mergedItems
# Phase 3: Sema + HIR + C codegen on unified module
let (semaRes, semaCtx) = analyzeFull(unifiedModule)
if semaRes.hasErrors:
+10 -5
View File
@@ -773,7 +773,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
return ctx.flushPending(ctx.lowerExpr(stmt.stmtExpr))
of skLet:
let initHir = ctx.lowerExpr(stmt.stmtLetInit)
var initHir: HirNode = nil
if stmt.stmtLetInit != nil:
initHir = ctx.lowerExpr(stmt.stmtLetInit)
let allocaType = if stmt.stmtLetType != nil:
case stmt.stmtLetType.kind
of tekNamed:
@@ -785,16 +787,17 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
let elemType = ctx.resolveTypeExpr(stmt.stmtLetType.sliceElement)
makeSlice(elemType)
else: makeUnknown()
else:
elif stmt.stmtLetInit != nil:
ctx.resolveExprType(stmt.stmtLetInit)
else:
makeUnknown()
let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc)
let varNode = hirVar(stmt.stmtLetName, makePointer(allocaType), loc)
let store = hirStore(varNode, initHir, loc)
# Track type expr for generic method inference
if stmt.stmtLetType != nil:
ctx.varTypeExprs[stmt.stmtLetName] = stmt.stmtLetType
elif stmt.stmtLetInit.kind == ekStructInit and stmt.stmtLetInit.exprStructInitTypeArgs.len > 0:
elif stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekStructInit and stmt.stmtLetInit.exprStructInitTypeArgs.len > 0:
ctx.varTypeExprs[stmt.stmtLetName] = TypeExpr(
kind: tekNamed,
loc: stmt.stmtLetInit.loc,
@@ -804,7 +807,9 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
var stmts = ctx.pendingStmts
ctx.pendingStmts = @[]
stmts.add(alloca)
stmts.add(store)
if initHir != nil:
let store = hirStore(varNode, initHir, loc)
stmts.add(store)
return hirBlock(stmts, nil, makeVoid(), loc)
of skReturn:
+55 -4
View File
@@ -93,6 +93,25 @@ proc expect(p: var Parser, kind: TokenKind, message: string): Token =
))
result = tok
proc isKeywordToken(kind: TokenKind): bool =
return kind in {tkIf, tkElse, tkWhile, tkDo, tkLoop, tkFor, tkIn, tkBreak,
tkContinue, tkReturn, tkMatch, tkFunc, tkLet, tkVar, tkConst, tkType,
tkStruct, tkEnum, tkUnion, tkInterface, tkExtend, tkModule, tkImport,
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkSizeOf, tkOwn,
tkDiscard}
proc expectIdentOrKeyword(p: var Parser, message: string): Token =
## Accept identifier OR keyword token as a name (for field names, param names, etc.)
if p.check(tkIdent) or isKeywordToken(p.at.kind):
return p.advance()
let tok = p.at
p.diagnostics.add(ParserDiagnostic(
severity: pdsError,
loc: tok.loc,
message: message & " (got " & tokenKindName(tok.kind) & ")"
))
result = tok
proc previous(p: Parser): Token =
if p.pos > 0 and p.pos <= p.tokens.len:
return p.tokens[p.pos - 1]
@@ -104,6 +123,9 @@ proc currentLoc(p: Parser): SourceLocation =
proc emitError(p: var Parser, loc: SourceLocation, message: string) =
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: loc, message: message))
proc skipNewlines(p: var Parser) =
while p.check(tkNewLine): discard p.advance()
proc emitError(p: var Parser, message: string) =
p.emitError(p.currentLoc, message)
@@ -492,7 +514,7 @@ proc parsePostfix(p: var Parser): Expr =
of tkDot:
# Field expression
discard p.advance()
let fieldName = p.expect(tkIdent, "expected field name after '.'").text
let fieldName = p.expectIdentOrKeyword("expected field name after '.'").text
left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: fieldName)
of tkPlusPlus, tkMinusMinus:
let op = p.advance().kind
@@ -656,19 +678,25 @@ proc parseBitOr(p: var Parser): Expr =
proc parseAnd(p: var Parser): Expr =
let loc = p.currentLoc
var left = p.parseBitOr()
p.skipNewlines()
while p.check(tkAmpAmp):
let op = p.advance().kind
p.skipNewlines()
let right = p.parseBitOr()
left = newBinaryExpr(op, left, right, loc)
p.skipNewlines()
return left
proc parseOr(p: var Parser): Expr =
let loc = p.currentLoc
var left = p.parseAnd()
p.skipNewlines()
while p.check(tkPipePipe):
let op = p.advance().kind
p.skipNewlines()
let right = p.parseAnd()
left = newBinaryExpr(op, left, right, loc)
p.skipNewlines()
return left
proc parseTernary(p: var Parser): Expr =
@@ -725,8 +753,12 @@ proc parseStmt(p: var Parser): Stmt =
if p.check(tkColon):
discard p.advance()
ty = p.parseType()
discard p.expect(tkAssign, "expected '=' in let/var statement")
let initExpr = p.parseExpr()
var initExpr: Expr = nil
if p.check(tkAssign):
discard p.advance()
initExpr = p.parseExpr()
elif not isMut:
discard p.expect(tkAssign, "expected '=' in let statement")
if p.check(tkSemicolon):
discard p.advance()
return Stmt(kind: skLet, loc: loc, stmtLetMut: isMut, stmtLetName: name,
@@ -749,6 +781,7 @@ proc parseStmt(p: var Parser): Stmt =
let elifCond = p.parseExpr()
let elifBlk = p.parseBlock()
elseIfs.add(ElseIf(loc: elseLoc, cond: elifCond, blk: elifBlk))
while p.check(tkNewLine): discard p.advance()
else:
elseBlk = p.parseBlock()
break
@@ -831,6 +864,20 @@ proc parseStmt(p: var Parser): Stmt =
if p.check(tkSemicolon):
discard p.advance()
return Stmt(kind: skContinue, loc: loc, stmtContinueLabel: label)
of tkDiscard:
discard p.advance()
var val: Expr = nil
if not p.check(tkSemicolon) and not p.check(tkRBrace) and not p.check(tkNewLine):
val = p.parseExpr()
if p.check(tkSemicolon):
discard p.advance()
# discard expr → expression statement; discard; → no-op (nil expr)
if val != nil:
return Stmt(kind: skExpr, loc: loc, stmtExpr: val)
else:
# No-op: emit literal 0 as expression statement
let zeroTok = Token(kind: tkIntLiteral, text: "0", loc: loc)
return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekLiteral, loc: loc, exprLit: zeroTok))
of tkFunc, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend, tkModule,
tkImport, tkConst, tkType, tkExtern, tkPub:
# Local declaration
@@ -931,17 +978,21 @@ proc parseStructDecl(p: var Parser, isPublic: bool): Decl =
discard p.advance()
if p.check(tkRBrace) or p.isAtEnd:
break
let startPos = p.pos # Track position for infinite-loop safeguard
let fLoc = p.currentLoc
var fPub = false
if p.check(tkPub):
fPub = true
discard p.advance()
let fName = p.expect(tkIdent, "expected field name").text
let fName = p.expectIdentOrKeyword("expected field name").text
discard p.expect(tkColon, "expected ':' after field name")
let fType = p.parseType()
if p.check(tkSemicolon) or p.check(tkComma):
discard p.advance()
fields.add(StructField(loc: fLoc, isPublic: fPub, name: fName, ftype: fType))
# Infinite-loop safeguard: if no progress, advance
if p.pos == startPos:
discard p.advance()
discard p.expect(tkRBrace, "expected '}' to close struct")
return Decl(kind: dkStruct, loc: loc, isPublic: isPublic,
declStructName: name, declStructTypeParams: typeParams,
+23 -4
View File
@@ -209,7 +209,19 @@ proc collectGlobals*(sema: var Sema) =
let retType = if decl.declFuncReturnType != nil: sema.resolveType(decl.declFuncReturnType) else: makeVoid()
sym.typ = makeFunc(params, retType)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declFuncName}'")
let existing = sema.globalScope.lookup(decl.declFuncName)
if existing != nil and existing.kind == skFunc:
if existing.decl != nil and existing.decl.declFuncBody == nil and decl.declFuncBody != nil:
# First was forward declaration, update with definition
existing.decl = decl
existing.typ = sym.typ
elif decl.declFuncBody == nil:
# New one is a forward declaration, existing already has it — skip
discard
else:
sema.emitError(decl.loc, &"duplicate symbol '{decl.declFuncName}'")
else:
sema.emitError(decl.loc, &"duplicate symbol '{decl.declFuncName}'")
# Auto-register func Type_Method(self: Type, ...) as a method
if decl.declFuncParams.len > 0 and decl.declFuncParams[0].name == "self":
var typeName = ""
@@ -243,7 +255,10 @@ proc collectGlobals*(sema: var Sema) =
let retType = if decl.declExtFuncReturnType != nil: sema.resolveType(decl.declExtFuncReturnType) else: makeVoid()
sym.typ = makeFunc(params, retType)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declExtFuncName}'")
# Allow duplicate extern func declarations (same func declared in multiple files)
let existing = sema.globalScope.lookup(decl.declExtFuncName)
if existing == nil or existing.kind != skFunc:
sema.emitError(decl.loc, &"duplicate symbol '{decl.declExtFuncName}'")
of dkStruct:
let t = makeNamed(decl.declStructName)
let sym = Symbol(kind: skType, name: decl.declStructName, typ: t,
@@ -776,10 +791,14 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
of skExpr:
return sema.checkExpr(stmt.stmtExpr, scope)
of skLet:
let initType = sema.checkExpr(stmt.stmtLetInit, scope)
var initType: Type = makeVoid()
if stmt.stmtLetInit != nil:
initType = sema.checkExpr(stmt.stmtLetInit, scope)
let declaredType = if stmt.stmtLetType != nil: sema.resolveType(stmt.stmtLetType) else: initType
if stmt.stmtLetType != nil and not initType.isAssignableTo(declaredType) and not (initType.kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}):
if stmt.stmtLetInit != nil and stmt.stmtLetType != nil and not initType.isAssignableTo(declaredType) and not (initType.kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}):
sema.emitError(stmt.loc, &"cannot assign {initType.toString} to {declaredType.toString}")
if stmt.stmtLetInit == nil and stmt.stmtLetType == nil:
sema.emitError(stmt.loc, "variable must have either type annotation or initializer")
let sym = Symbol(kind: skVar, name: stmt.stmtLetName, typ: declaredType,
isMutable: stmt.stmtLetMut)
if not scope.define(sym):
+4 -1
View File
@@ -50,6 +50,7 @@ type
tkSuper # super
tkSizeOf # sizeof
tkOwn # own (gradual ownership transfer)
tkDiscard # discard (evaluate and throw away)
##Punctuation
tkLParen # (
@@ -141,7 +142,7 @@ proc isKeyword*(kind: TokenKind): bool =
tkBreak, tkContinue, tkReturn, tkMatch,
tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum,
tkUnion, tkInterface, tkExtend, tkModule, tkImport,
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn:
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkDiscard:
true
else:
false
@@ -196,6 +197,7 @@ proc keywordKind*(text: string): TokenKind =
of "super": tkSuper
of "sizeof": tkSizeOf
of "own": tkOwn
of "discard": tkDiscard
of "true", "false": tkBoolLiteral
else: tkIdent
@@ -240,6 +242,7 @@ proc tokenKindName*(kind: TokenKind): string =
of tkSelf: "'self'"
of tkSuper: "'super'"
of tkOwn: "'own'"
of tkDiscard: "'discard'"
of tkLParen: "'('"
of tkRParen: "')'"
of tkLBrace: "'{'"