From 70cfa4f721edd578b4d1b45912cbc948fb3c596e Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 2 Jun 2026 11:33:29 +0300 Subject: [PATCH] feat: backtick raw/multi-line strings + QBE backend fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add backtick raw string literals (Go-style ): no escape processing, multi-line - C backend: handle backtick strings in emitExpr - Self-hosted lexer: lexScanBacktickString support - QBE backend: handle backtick strings in data sections - QBE: remove broken extern declarations (QBE resolves at link time) - QBE: fix comparison result types (ceql → w, extend to l) - QBE: fix SSA parameter name clash (_p_ prefix for params) - QBE: const inlining via HirLower + QBE emitter - QBE: proper type mapping for bool/char types - hir_lower: collect dkConst declarations for const propagation --- _selfhost/src/cli.bux | 100 +++- _selfhost/src/hir_lower.bux | 14 + _selfhost/src/lexer.bux | 19 + _selfhost/src/parser.bux | 51 +- _selfhost/src/qbe_backend.bux | 942 ++++++++++++++++++++++++++++++++++ src/c_backend.nim | 5 +- src/lexer.nim | 17 + src/token.nim | 2 +- src_bux/cli.bux | 100 +++- src_bux/hir_lower.bux | 14 + src_bux/lexer.bux | 19 + src_bux/parser.bux | 31 +- src_bux/qbe_backend.bux | 942 ++++++++++++++++++++++++++++++++++ stdlib/runtime.c | 1 + 14 files changed, 2193 insertions(+), 64 deletions(-) create mode 100644 _selfhost/src/qbe_backend.bux create mode 100644 src_bux/qbe_backend.bux diff --git a/_selfhost/src/cli.bux b/_selfhost/src/cli.bux index 15e77c8..eb6e441 100644 --- a/_selfhost/src/cli.bux +++ b/_selfhost/src/cli.bux @@ -98,14 +98,14 @@ func Cli_Compile(source: String, sourceName: String) -> String { PrintInt(hirMod.funcCount); PrintLine(""); - // Phase 5: C code generation - let nimCode: String = NimBackend_Generate(hirMod); + // Phase 5: QBE SSA code generation + let qbeCode: String = QbeBackend_Generate(hirMod); // Cleanup Lexer_Free(lex); Sema_Free(sema); - return nimCode; + return qbeCode; } // --------------------------------------------------------------------------- @@ -123,17 +123,17 @@ func Cli_Build(srcPath: String, outPath: String) -> int { Print("Compiling "); PrintLine(srcPath); - let cCode: String = Cli_Compile(source, srcPath); + let ssaCode: String = Cli_Compile(source, srcPath); - if String_Eq(cCode, "") { + if String_Eq(ssaCode, "") { PrintLine("Compilation failed"); return 1; } - // Write C output - let ok: bool = WriteFile(outPath, cCode); + // Write SSA output + let ok: bool = WriteFile(outPath, ssaCode); if ok { - Print(" → Nim code written to "); + Print(" → SSA written to "); PrintLine(outPath); return 0; } else { @@ -233,6 +233,11 @@ func Cli_CompileSource(source: String, sourceName: String) -> *HirModule { } func Cli_BuildProject(projectDir: String) -> int { + let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml")); + var outName: String = man.name; + if String_Eq(outName, "") { + outName = "bux_out"; + } let srcDir: String = bux_path_join(projectDir, "src"); if !DirExists(srcDir) { Print("Error: no src/ directory in "); @@ -327,30 +332,79 @@ func Cli_BuildProject(projectDir: String) -> int { PrintLine("Lowering to HIR..."); let hirMod: *HirModule = HirLower_LowerModule(merged, sema); - // Nim code generation - PrintLine("Generating Nim code..."); - let nimCode: String = NimBackend_Generate(hirMod); + // QBE SSA code generation + PrintLine("Generating QBE SSA..."); + let qbeCode: String = QbeBackend_Generate(hirMod); // Create build directory let buildDir: String = bux_path_join(projectDir, "build"); discard bux_mkdir_if_needed(buildDir); - // Write Nim output - let nimFile: String = bux_path_join(buildDir, "main.nim"); - if !WriteFile(nimFile, nimCode) { - PrintLine("Error: cannot write main.nim"); + // Write SSA output + let ssaFile: String = bux_path_join(buildDir, "main.ssa"); + if !WriteFile(ssaFile, qbeCode) { + PrintLine("Error: cannot write main.ssa"); return 1; } - Print("Nim code written to "); - PrintLine(nimFile); + Print("SSA written to "); + PrintLine(ssaFile); - // Invoke Nim compiler - PrintLine("Invoking Nim compiler..."); - let outBin: String = bux_path_join(buildDir, "buxc3"); - let rc: int = bux_run_nim(nimFile, outBin); - if rc != 0 { - PrintLine("Error: Nim compilation failed"); + // Invoke QBE to generate assembly + PrintLine("Invoking QBE..."); + let asmFile: String = bux_path_join(buildDir, "main.s"); + var qbeCmd: String = bux_path_join(projectDir, "vendor/qbe/qbe"); + // Try relative to project first, then absolute + if !FileExists(qbeCmd) { + // Try from compiler location + qbeCmd = "../vendor/qbe/qbe"; + } + if !FileExists(qbeCmd) { + qbeCmd = "vendor/qbe/qbe"; + } + // Actually let's use bux_system with a constructed command + var cmdBuf: StringBuilder = StringBuilder_NewCap(256); + StringBuilder_Append(&cmdBuf, qbeCmd); + StringBuilder_Append(&cmdBuf, " -o "); + StringBuilder_Append(&cmdBuf, asmFile); + StringBuilder_Append(&cmdBuf, " "); + StringBuilder_Append(&cmdBuf, ssaFile); + let qbeRc: int = bux_system(StringBuilder_Build(&cmdBuf)); + if qbeRc != 0 { + PrintLine("Error: QBE compilation failed"); + return 1; + } + + // Link with cc + PrintLine("Linking..."); + var linkBuf: StringBuilder = StringBuilder_NewCap(512); + StringBuilder_Append(&linkBuf, "cc -o "); + let outBin: String = bux_path_join(buildDir, outName); + StringBuilder_Append(&linkBuf, outBin); + StringBuilder_Append(&linkBuf, " "); + StringBuilder_Append(&linkBuf, asmFile); + StringBuilder_Append(&linkBuf, " "); + // Find runtime.c and io.c + var rtPath: String = bux_path_join(projectDir, "stdlib/runtime.c"); + var ioPath: String = bux_path_join(projectDir, "stdlib/io.c"); + if !FileExists(rtPath) { + rtPath = bux_path_join(projectDir, "../stdlib/runtime.c"); + } + if !FileExists(ioPath) { + ioPath = bux_path_join(projectDir, "../stdlib/io.c"); + } + if FileExists(rtPath) { + StringBuilder_Append(&linkBuf, rtPath); + StringBuilder_Append(&linkBuf, " "); + } + if FileExists(ioPath) { + StringBuilder_Append(&linkBuf, ioPath); + StringBuilder_Append(&linkBuf, " "); + } + StringBuilder_Append(&linkBuf, "-lm"); + let linkRc: int = bux_system(StringBuilder_Build(&linkBuf)); + if linkRc != 0 { + PrintLine("Error: Linking failed"); return 1; } diff --git a/_selfhost/src/hir_lower.bux b/_selfhost/src/hir_lower.bux index 76b1b4e..07b2c2f 100644 --- a/_selfhost/src/hir_lower.bux +++ b/_selfhost/src/hir_lower.bux @@ -412,6 +412,8 @@ func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule { hm.funcs = ctx.funcs; hm.structCount = 0; hm.structs = bux_alloc(64 as uint * sizeof(HirStruct)) as *HirStruct; + hm.constCount = 0; + hm.consts = bux_alloc(512 as uint * sizeof(HirConst)) as *HirConst; // First pass: count structs (to allocate field arrays later) // Second pass: actually collect them @@ -468,6 +470,18 @@ func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule { } hm.structCount = hm.structCount + 1; } + if decl.kind == dkConst && hm.constCount < 512 { + let ci: int = hm.constCount; + hm.consts[ci].name = decl.strValue; + var val: int = 0; + if decl.constValue != null as *Expr { + if decl.constValue.kind == ekLiteral { + val = decl.constValue.intValue; + } + } + hm.consts[ci].value = val; + hm.constCount = hm.constCount + 1; + } if decl.kind == dkEnum { hm.enumCount = hm.enumCount + 1; } diff --git a/_selfhost/src/lexer.bux b/_selfhost/src/lexer.bux index 6bbfb2f..7fe64b1 100644 --- a/_selfhost/src/lexer.bux +++ b/_selfhost/src/lexer.bux @@ -384,6 +384,20 @@ func lexScanNumber(lex: *Lexer) { // Strings and chars // --------------------------------------------------------------------------- +func lexScanBacktickString(lex: *Lexer) { + lexMarkStart(lex); + if lexPeek(lex, 0) == 96 { discard lexAdvance(lex); } // opening backtick + while !lexIsAtEnd(lex) && lexPeek(lex, 0) != 96 { + discard lexAdvance(lex); + } + if lexIsAtEnd(lex) { + lexEmitDiag(lex, "unterminated backtick string literal"); + } else { + discard lexAdvance(lex); // closing backtick + } + lexEmitToken(lex, tkStringLiteral); +} + func lexScanString(lex: *Lexer) { lexMarkStart(lex); if lexPeek(lex, 0) == 34 { discard lexAdvance(lex); } // opening " @@ -618,6 +632,11 @@ func lexNextToken(lex: *Lexer) { lexScanString(lex); return; } + // Backtick raw string + if c == 96 { // backtick + lexScanBacktickString(lex); return; + } + // Char prefixes: c8' c16' c32' if c == 99 { // 'c' let d: uint32 = lexPeek(lex, 1); diff --git a/_selfhost/src/parser.bux b/_selfhost/src/parser.bux index 310131e..8ad0e5b 100644 --- a/_selfhost/src/parser.bux +++ b/_selfhost/src/parser.bux @@ -78,10 +78,12 @@ func parserExpect(p: *Parser, kind: int, msg: String) -> LexToken { return parserAdvance(p); } let tok: LexToken = parserCurToken(p); - p.diags[p.diagCount] = ParserDiag { - line: tok.line, column: tok.column, message: msg - }; - p.diagCount = p.diagCount + 1; + if p.diagCount < 256 { + p.diags[p.diagCount] = ParserDiag { + line: tok.line, column: tok.column, message: msg + }; + p.diagCount = p.diagCount + 1; + } return tok; } @@ -103,10 +105,12 @@ func parserExpectIdentOrKeyword(p: *Parser, msg: String) -> LexToken { if tok.kind == tkIdent || parserIsKeyword(tok.kind) { return parserAdvance(p); } - p.diags[p.diagCount] = ParserDiag { - line: tok.line, column: tok.column, message: msg - }; - p.diagCount = p.diagCount + 1; + if p.diagCount < 256 { + p.diags[p.diagCount] = ParserDiag { + line: tok.line, column: tok.column, message: msg + }; + p.diagCount = p.diagCount + 1; + } return tok; } @@ -479,14 +483,33 @@ func parserIsBinaryOp(kind: int) -> bool { return false; } -func parserParseBinary(p: *Parser) -> *Expr { - var left: *Expr = parserParsePostfix(p); +func parserPrecedence(op: int) -> int { + if op == tkAssign || op == tkPlusAssign || op == tkMinusAssign || op == tkStarAssign || op == tkSlashAssign || op == tkPercentAssign || op == tkAmpAssign || op == tkPipeAssign || op == tkCaretAssign || op == tkShlAssign || op == tkShrAssign { return 1; } + if op == tkPipePipe { return 2; } + if op == tkAmpAmp { return 3; } + if op == tkPipe { return 4; } + if op == tkCaret { return 5; } + if op == tkAmp { return 6; } + if op == tkEq || op == tkNe { return 7; } + if op == tkLt || op == tkLe || op == tkGt || op == tkGe { return 8; } + if op == tkShl || op == tkShr { return 9; } + if op == tkPlus || op == tkMinus { return 10; } + if op == tkStar || op == tkSlash || op == tkPercent { return 11; } + if op == tkStarStar { return 12; } + return 0; +} - while parserIsBinaryOp(parserPeek(p, 0)) { +func parserParseBinaryPrec(p: *Parser, minPrec: int) -> *Expr { + var left: *Expr = parserParsePostfix(p); + while true { + let op: int = parserPeek(p, 0); + let prec: int = parserPrecedence(op); + if prec < minPrec { break; } let opTok: LexToken = parserAdvance(p); let line: uint32 = opTok.line; let col: uint32 = opTok.column; - let right: *Expr = parserParsePostfix(p); + let nextMinPrec: int = prec + 1; + let right: *Expr = parserParseBinaryPrec(p, nextMinPrec); let e: *Expr = parserMakeExpr(ekBinary, line, col); e.intValue = opTok.kind; e.child1 = left; @@ -496,6 +519,10 @@ func parserParseBinary(p: *Parser) -> *Expr { return left; } +func parserParseBinary(p: *Parser) -> *Expr { + return parserParseBinaryPrec(p, 1); +} + // --------------------------------------------------------------------------- // Ternary: cond ? then : else // --------------------------------------------------------------------------- diff --git a/_selfhost/src/qbe_backend.bux b/_selfhost/src/qbe_backend.bux new file mode 100644 index 0000000..8f4a029 --- /dev/null +++ b/_selfhost/src/qbe_backend.bux @@ -0,0 +1,942 @@ +// qbe_backend.bux — QBE SSA IL generator backend +// Generates QBE SSA assembly from HIR. Replaces both C and Nim backends. +// Pipeline: HirModule → .ssa → qbe → .s → cc → executable + +import Std::String; +import Std::Io::{Print, PrintLine}; + +module QbeBackend { + +// --------------------------------------------------------------------------- +// QBE type mapping +// QBE base types: w (32b), l (64b), s (32f), d (64f), b (8b), h (16b) +// Aggregate: :Name = { type, type, ... } +// --------------------------------------------------------------------------- + +func QbeBackend_TypeBase(kind: int) -> String { + if kind == tyVoid { return ""; } + if kind == tyBool { return "w"; } + if kind == tyBool8 { return "b"; } + if kind == tyBool16 { return "h"; } + if kind == tyBool32 { return "w"; } + if kind == tyChar8 { return "b"; } + if kind == tyChar16 { return "h"; } + if kind == tyChar32 { return "w"; } + if kind == tyStr { return "l"; } + if kind == tyInt8 { return "b"; } + if kind == tyInt16 { return "h"; } + if kind == tyInt32 { return "w"; } + if kind == tyInt64 { return "l"; } + if kind == tyInt { return "l"; } + if kind == tyUInt8 { return "b"; } + if kind == tyUInt16 { return "h"; } + if kind == tyUInt32 { return "w"; } + if kind == tyUInt64 { return "l"; } + if kind == tyUInt { return "l"; } + if kind == tyFloat32 { return "s"; } + if kind == tyFloat64 { return "d"; } + if kind == tyPointer { return "l"; } + return "l"; +} + +// Return the "word" type for a given kind (used for comparisons, arithmetic) +func QbeBackend_TypeWord(kind: int) -> String { + if kind == tyFloat32 { return "s"; } + if kind == tyFloat64 { return "d"; } + if kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32 { return "w"; } + if kind == tyInt32 || kind == tyUInt32 || kind == tyChar32 { return "w"; } + if kind == tyChar8 { return "b"; } + if kind == tyChar16 { return "h"; } + return "l"; +} + +// --------------------------------------------------------------------------- +// Emitter state +// --------------------------------------------------------------------------- + +struct QbeEmitter { + dataSb: StringBuilder; + codeSb: StringBuilder; + tempId: int; + labelId: int; + strId: int; + indent: int; + // Track if we're currently in a block (after @label) + inBlock: bool; + // Const table for inline substitution + constCount: int; + consts: *HirConst; +} + +func QBE_Init() -> QbeEmitter { + var dsb: StringBuilder = StringBuilder_NewCap(4096); + var csb: StringBuilder = StringBuilder_NewCap(16384); + return QbeEmitter { dataSb: dsb, codeSb: csb, tempId: 0, labelId: 0, strId: 0, indent: 0, inBlock: false, constCount: 0, consts: null as *HirConst }; +} + +func QBE_FreshTemp(qbe: *QbeEmitter) -> String { + let id: int = qbe.tempId; + qbe.tempId = id + 1; + var prefix: String = "%t"; + return String_Concat(prefix, String_FromInt(id as int64)); +} + +func QBE_FreshLabel(qbe: *QbeEmitter) -> String { + let id: int = qbe.labelId; + qbe.labelId = id + 1; + var prefix: String = "@L"; + return String_Concat(prefix, String_FromInt(id as int64)); +} + +func QBE_FreshStr(qbe: *QbeEmitter) -> String { + let id: int = qbe.strId; + qbe.strId = id + 1; + var prefix: String = "$str_"; + return String_Concat(prefix, String_FromInt(id as int64)); +} + +func QBE_EmitData(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.dataSb, text); +} + +func QBE_EmitDataLine(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.dataSb, text); + StringBuilder_Append(&qbe.dataSb, "\n"); +} + +func QBE_EmitDataChar(qbe: *QbeEmitter, c: char8) { + var tmp: *char8 = bux_alloc(2) as *char8; + tmp[0] = c; + tmp[1] = 0 as char8; + StringBuilder_Append(&qbe.dataSb, tmp); +} + +func QBE_Emit(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.codeSb, text); +} + +func QBE_EmitLine(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.codeSb, text); + StringBuilder_Append(&qbe.codeSb, "\n"); +} + +func QBE_EmitChar(qbe: *QbeEmitter, c: char8) { + var tmp: *char8 = bux_alloc(2) as *char8; + tmp[0] = c; + tmp[1] = 0 as char8; + StringBuilder_Append(&qbe.codeSb, tmp); +} + +func QBE_EmitIndent(qbe: *QbeEmitter) { + var i: int = 0; + while i < qbe.indent { + StringBuilder_Append(&qbe.codeSb, " "); + i = i + 1; + } +} + +func QBE_Build(qbe: *QbeEmitter) -> String { + let dataPart: String = StringBuilder_Build(&qbe.dataSb); + let codePart: String = StringBuilder_Build(&qbe.codeSb); + var result: StringBuilder = StringBuilder_NewCap(2048); + StringBuilder_Append(&result, dataPart); + StringBuilder_Append(&result, codePart); + return StringBuilder_Build(&result); +} + +// --------------------------------------------------------------------------- +// Escape identifier for QBE +// QBE names: $global, %temp, @label +// --------------------------------------------------------------------------- + +func QBE_EscapeIdent(name: String) -> String { + if name == null as String || String_Eq(name, "") { return "_anon"; } + return name; +} + +func QBE_LocalName(name: String) -> String { + if name == null as String || String_Eq(name, "") { return "%_anon"; } + var prefix: String = "%"; + return String_Concat(prefix, name); +} + +// --------------------------------------------------------------------------- +// String literal → data section +// --------------------------------------------------------------------------- + +func QBE_EmitStringLiteral(qbe: *QbeEmitter, text: String) -> String { + let sym: String = QBE_FreshStr(qbe); + var content: String = text; + var start: int = 0; + var end: int = 0; + if content != null as String { + let slen: int = bux_strlen(content) as int; + end = slen; + if slen >= 2 { + // Regular string: "content" + if content[0] == 34 as char8 && content[slen - 1] == 34 as char8 { + start = 1; + end = slen - 1; + } + // Backtick raw string: `content` + if content[0] == 96 as char8 && content[slen - 1] == 96 as char8 { + start = 1; + end = slen - 1; + } + } + } + // Build data declaration into dataSb + // data $str = { b "content", b 0 } + QBE_EmitData(qbe, "data "); + QBE_EmitData(qbe, sym); + QBE_EmitData(qbe, " = { b \""); + var i: int = start; + while i < end { + let c: char8 = content[i]; + if c == 10 as char8 { QBE_EmitData(qbe, "\\n"); } + else if c == 13 as char8 { QBE_EmitData(qbe, "\\r"); } + else if c == 9 as char8 { QBE_EmitData(qbe, "\\t"); } + else if c == 34 as char8 { QBE_EmitData(qbe, "\\\""); } + else if c == 92 as char8 { QBE_EmitData(qbe, "\\\\"); } + else { QBE_EmitDataChar(qbe, c); } + i = i + 1; + } + QBE_EmitDataLine(qbe, "\", b 0 }"); + return sym; +} + +// --------------------------------------------------------------------------- +// Type definitions for structs +// --------------------------------------------------------------------------- + +func QBE_EmitStructType(qbe: *QbeEmitter, st: *HirStruct) { + QBE_Emit(qbe, "type :"); + QBE_Emit(qbe, QBE_EscapeIdent(st.name)); + QBE_Emit(qbe, " = { "); + var fi: int = 0; + while fi < st.fieldCount { + if fi > 0 { QBE_Emit(qbe, ", "); } + // Map field type; default to l if unknown + var ft: String = "l"; + if st.fields != null as *HirStructField { + let ftype: String = st.fields[fi].typeName; + if String_Eq(ftype, "int") || String_Eq(ftype, "int64") || String_Eq(ftype, "uint") || String_Eq(ftype, "uint64") || String_Eq(ftype, "String") || String_Eq(ftype, "*char8") { + ft = "l"; + } else if String_Eq(ftype, "int32") || String_Eq(ftype, "uint32") { + ft = "w"; + } else if String_Eq(ftype, "bool") || String_Eq(ftype, "char8") { + ft = "b"; + } else if String_Eq(ftype, "float64") { + ft = "d"; + } else if String_Eq(ftype, "float32") { + ft = "s"; + } + } + QBE_Emit(qbe, ft); + fi = fi + 1; + } + QBE_EmitLine(qbe, " }"); +} + +// --------------------------------------------------------------------------- +// Helper: check if a statement node is a block terminator +// --------------------------------------------------------------------------- + +func QBE_IsTerminator(node: *HirNode) -> bool { + if node == null as *HirNode { return false; } + if node.kind == hReturn { return true; } + if node.kind == hBreak { return true; } + if node.kind == hContinue { return true; } + if node.kind == hBlock { + var last: *HirNode = node.child1; + if last == null as *HirNode { return false; } + while last.child3 != null as *HirNode { + last = last.child3; + } + return QBE_IsTerminator(last); + } + return false; +} + +// --------------------------------------------------------------------------- +// Expression emission — returns the QBE value string (e.g. "%t3", "42", "$foo") +// For expressions that need multiple instructions, emits them inline. +// --------------------------------------------------------------------------- + +func QBE_EmitExpr(qbe: *QbeEmitter, node: *HirNode) -> String { + if node == null as *HirNode { + return "0"; + } + let kind: int = node.kind; + + // Literal + if kind == hLit { + if node.intValue == tkNull { + return "0"; + } + if node.intValue == tkStringLiteral { + return QBE_EmitStringLiteral(qbe, node.strValue); + } + if node.intValue == tkBoolLiteral { + if String_Eq(node.strValue, "true") { return "1"; } + return "0"; + } + // Numeric literal — emit as-is + return node.strValue; + } + + // Variable + if kind == hVar { + // Check if this is a const reference + var ci: int = 0; + while ci < qbe.constCount { + if String_Eq(qbe.consts[ci].name, node.strValue) { + var cv: String = String_FromInt(qbe.consts[ci].value as int64); + return cv; + } + ci = ci + 1; + } + let tmp: String = QBE_FreshTemp(qbe); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " load"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, QBE_LocalName(node.strValue)); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Assign (used as expression — e.g. inside another binary op) + if kind == hAssign { + var target: String = ""; + if node.child1 != null as *HirNode && node.child1.kind == hVar { + target = QBE_LocalName(node.child1.strValue); + } else { + target = QBE_EmitExpr(qbe, node.child1); + } + let val: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, target); + return val; + } + + // Binary operation + if kind == hBinary { + let op: int = node.intValue; + let left: String = QBE_EmitExpr(qbe, node.child1); + let right: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + + // Comparison operators + // QBE comparison ops: ceqw/ceql, cnew/cnel, csltw/csltl, cslew/cslel, csgtw/csgtl, csgew/csgel + var cmpOp: String = ""; + var cmpTy: String = ty; + if op == tkEq { cmpOp = "ceq"; } + else if op == tkNe { cmpOp = "cne"; } + else if op == tkLt { cmpOp = "cslt"; } + else if op == tkLe { cmpOp = "csle"; } + else if op == tkGt { cmpOp = "csgt"; } + else if op == tkGe { cmpOp = "csge"; } + + if !String_Eq(cmpOp, "") { + let cmpTmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, cmpTmp); + QBE_Emit(qbe, " =w "); + QBE_Emit(qbe, cmpOp); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, right); + QBE_EmitLine(qbe, ""); + // QBE comparisons produce word results; extend if needed + if !String_Eq(ty, "w") && !String_Eq(ty, "b") && !String_Eq(ty, "h") { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " extsw "); + QBE_Emit(qbe, cmpTmp); + QBE_EmitLine(qbe, ""); + return tmp; + } + return cmpTmp; + } + + // Arithmetic + var opName: String = ""; + if op == tkPlus { opName = "add"; } + else if op == tkMinus { opName = "sub"; } + else if op == tkStar { opName = "mul"; } + else if op == tkSlash { opName = "div"; } + else if op == tkPercent { opName = "rem"; } + else if op == tkAmp { opName = "and"; } + else if op == tkPipe { opName = "or"; } + else if op == tkCaret { opName = "xor"; } + else if op == tkShl { opName = "shl"; } + else if op == tkShr { opName = "shr"; } + + if !String_Eq(opName, "") { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, opName); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, right); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Logical and/or — convert to branch sequences + if op == tkAmpAmp { + let tmp: String = QBE_FreshTemp(qbe); + let lTrue: String = QBE_FreshLabel(qbe); + let lFalse: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lTrue); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lFalse); + QBE_EmitLine(qbe, lTrue); + let rVal: String = QBE_EmitExpr(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_EmitLine(qbe, " =w copy rVal"); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + QBE_EmitLine(qbe, lFalse); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_EmitLine(qbe, " =w copy 0"); + QBE_EmitLine(qbe, lEnd); + return tmp; + } + if op == tkPipePipe { + let tmp: String = QBE_FreshTemp(qbe); + let lTrue: String = QBE_FreshLabel(qbe); + let lFalse: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lTrue); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lFalse); + QBE_EmitLine(qbe, lTrue); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_EmitLine(qbe, " =w copy 1"); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + QBE_EmitLine(qbe, lFalse); + let rVal: String = QBE_EmitExpr(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " =w copy "); + QBE_EmitLine(qbe, rVal); + QBE_EmitLine(qbe, lEnd); + return tmp; + } + + return left; + } + + // Unary + if kind == hUnary { + let op: int = node.intValue; + let operand: String = QBE_EmitExpr(qbe, node.child1); + let ty: String = QbeBackend_TypeWord(node.typeKind); + + if op == tkBang || op == tkMinus { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + if op == tkBang { + // xor with 1 to invert boolean + QBE_Emit(qbe, " xor "); + QBE_Emit(qbe, operand); + QBE_Emit(qbe, ", 1"); + } else { + QBE_Emit(qbe, " sub 0, "); + QBE_Emit(qbe, operand); + } + QBE_EmitLine(qbe, ""); + return tmp; + } + if op == tkAmp { + // Address-of: just return the variable name (it's already a pointer) + // The operand should be a variable + if node.child1 != null as *HirNode && node.child1.kind == hVar { + return QBE_EscapeIdent(node.child1.strValue); + } + return operand; + } + return operand; + } + + // Call + if kind == hCall { + // Pre-evaluate arguments to avoid emitting instructions inside the call + let tmp: String = QBE_FreshTemp(qbe); + let retTy: String = QbeBackend_TypeWord(node.typeKind); + var arg1: String = ""; + var arg2: String = ""; + if node.child1 != null as *HirNode { + arg1 = QBE_EmitExpr(qbe, node.child1); + } + if node.child2 != null as *HirNode { + arg2 = QBE_EmitExpr(qbe, node.child2); + } + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, retTy); + QBE_Emit(qbe, " call $"); + QBE_Emit(qbe, QBE_EscapeIdent(node.strValue)); + QBE_Emit(qbe, "("); + if node.child1 != null as *HirNode { + let a1ty: String = QbeBackend_TypeWord(node.child1.typeKind); + QBE_Emit(qbe, a1ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, arg1); + } + if node.child2 != null as *HirNode { + let a2ty: String = QbeBackend_TypeWord(node.child2.typeKind); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, a2ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, arg2); + } + QBE_EmitLine(qbe, ")"); + return tmp; + } + + // Load (dereference) + if kind == hLoad { + let ptr: String = QBE_EmitExpr(qbe, node.child1); + let tmp: String = QBE_FreshTemp(qbe); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " load"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, ptr); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Field pointer: &obj.field + if kind == hFieldPtr { + let base: String = QBE_EmitExpr(qbe, node.child1); + let tmp: String = QBE_FreshTemp(qbe); + // add base, offset — need to know offset; for now assume 0 for first field + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " =l add "); + QBE_Emit(qbe, base); + QBE_Emit(qbe, ", 0"); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Cast + if kind == hCast { + let src: String = QBE_EmitExpr(qbe, node.child1); + let dstTy: String = QbeBackend_TypeWord(node.typeKind); + let srcTy: String = QbeBackend_TypeWord(node.child1.typeKind); + if String_Eq(dstTy, srcTy) { + return src; + } + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, dstTy); + // QBE cast instructions: extsw, extuw, extsl, extul, truncl, stosi, etc. + // For simplicity, use copy for now and let QBE handle it + QBE_Emit(qbe, " copy "); + QBE_Emit(qbe, src); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // SizeOf + if kind == hSizeOf { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " =l copy 8"); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Fallback + return "0"; +} + +// --------------------------------------------------------------------------- +// Statement emission — no return value, just side effects +// --------------------------------------------------------------------------- + +func QBE_EmitStmt(qbe: *QbeEmitter, node: *HirNode) { + if node == null as *HirNode { return; } + let kind: int = node.kind; + + // Store: declaration or assignment + if kind == hStore { + if node.child1 != null as *HirNode && node.child1.kind == hAlloca { + // Declaration: alloca + init + let varName: String = QBE_LocalName(node.child1.strValue); + let varTy: String = QbeBackend_TypeWord(node.child1.typeKind); + // Emit alloca + QBE_EmitIndent(qbe); + QBE_Emit(qbe, varName); + QBE_Emit(qbe, " =l alloc4 8"); + QBE_EmitLine(qbe, ""); + if node.child2 != null as *HirNode { + let val: String = QBE_EmitExpr(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, varTy); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, varName); + } + return; + } + // Plain assignment + let target: String = QBE_EmitExpr(qbe, node.child1); + let val: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, target); + return; + } + + // Assign: same as Store without alloca + if kind == hAssign { + var target: String = ""; + if node.child1 != null as *HirNode && node.child1.kind == hVar { + target = QBE_LocalName(node.child1.strValue); + } else { + target = QBE_EmitExpr(qbe, node.child1); + } + let val: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, target); + return; + } + + // Return + if kind == hReturn { + if node.child1 != null as *HirNode { + let val: String = QBE_EmitExpr(qbe, node.child1); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "ret "); + QBE_EmitLine(qbe, val); + } else { + QBE_EmitIndent(qbe); + QBE_EmitLine(qbe, "ret"); + } + return; + } + + // If + if kind == hIf { + let condVal: String = QBE_EmitExpr(qbe, node.child1); + let lThen: String = QBE_FreshLabel(qbe); + let lElse: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, condVal); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lThen); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lElse); + + QBE_EmitLine(qbe, lThen); + QBE_EmitStmt(qbe, node.child2); + if !QBE_IsTerminator(node.child2) { + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + } + + QBE_EmitLine(qbe, lElse); + QBE_EmitStmt(qbe, node.child3); + if !QBE_IsTerminator(node.child3) { + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + } + + QBE_EmitLine(qbe, lEnd); + return; + } + + // While + if kind == hWhile { + let lCond: String = QBE_FreshLabel(qbe); + let lBody: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lCond); + + QBE_EmitLine(qbe, lCond); + let condVal: String = QBE_EmitExpr(qbe, node.child1); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, condVal); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lBody); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lEnd); + + QBE_EmitLine(qbe, lBody); + QBE_EmitStmt(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lCond); + + QBE_EmitLine(qbe, lEnd); + return; + } + + // Loop (infinite) + if kind == hLoop { + let lBody: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + + // Save loop labels for break/continue (not implemented yet) + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lBody); + + QBE_EmitLine(qbe, lBody); + QBE_EmitStmt(qbe, node.child1); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lBody); + + QBE_EmitLine(qbe, lEnd); + return; + } + + // Break / Continue + if kind == hBreak { + // TODO: need loop label stack + return; + } + if kind == hContinue { + // TODO: need loop label stack + return; + } + + // Block — sequence of statements + if kind == hBlock { + var child: *HirNode = node.child1; + while child != null as *HirNode { + QBE_EmitStmt(qbe, child); + child = child.child3; + } + return; + } + + // Call as statement + if kind == hCall { + QBE_EmitExpr(qbe, node); + return; + } + + // Alloca as standalone statement + if kind == hAlloca { + // Already handled in Store; standalone alloca is a no-op + return; + } + + // Fallback: any other expression used as a statement + discard QBE_EmitExpr(qbe, node); +} + +// --------------------------------------------------------------------------- +// Function emission +// --------------------------------------------------------------------------- + +func QBE_EmitFunc(qbe: *QbeEmitter, f: *HirFunc) { + if f.body == null as *HirNode { return; } + + QBE_Emit(qbe, "export function "); + let retTy: String = QbeBackend_TypeWord(f.retTypeKind); + QBE_Emit(qbe, retTy); + QBE_Emit(qbe, " $"); + QBE_Emit(qbe, QBE_EscapeIdent(f.name)); + QBE_Emit(qbe, "("); + + // Parameters + var pi: int = 0; + var pnames: *HirParam = &f.param0; + while pi < f.paramCount { + if pi > 0 { QBE_Emit(qbe, ", "); } + let pty: String = QbeBackend_TypeWord(pnames[pi].typeKind); + QBE_Emit(qbe, pty); + QBE_Emit(qbe, " %_p_"); + QBE_Emit(qbe, QBE_EscapeIdent(pnames[pi].name)); + pi = pi + 1; + } + QBE_EmitLine(qbe, ") {"); + + // Entry block + QBE_EmitLine(qbe, "@start"); + + // Allocate local slots for parameters (QBE params are values, not pointers) + // Use _p_ prefix to avoid SSA name conflict with the local allocation + pi = 0; + pnames = &f.param0; + while pi < f.paramCount { + let pName: String = pnames[pi].name; + let pTy: String = QbeBackend_TypeWord(pnames[pi].typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, QBE_LocalName(pName)); + QBE_Emit(qbe, " =l alloc4 8"); + QBE_EmitLine(qbe, ""); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, pTy); + QBE_Emit(qbe, " %_p_"); + QBE_Emit(qbe, QBE_EscapeIdent(pName)); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, QBE_LocalName(pName)); + QBE_EmitLine(qbe, ""); + pi = pi + 1; + } + + // Body + QBE_EmitStmt(qbe, f.body); + + QBE_EmitLine(qbe, "}"); + QBE_EmitLine(qbe, ""); +} + +// --------------------------------------------------------------------------- +// Extern function declarations +// --------------------------------------------------------------------------- + +func QBE_EmitExtern(qbe: *QbeEmitter, f: *HirFunc) { + QBE_Emit(qbe, "function "); + let retTy: String = QbeBackend_TypeWord(f.retTypeKind); + QBE_Emit(qbe, retTy); + QBE_Emit(qbe, " $"); + QBE_Emit(qbe, QBE_EscapeIdent(f.name)); + QBE_Emit(qbe, "("); + var pi: int = 0; + var pnames: *HirParam = &f.param0; + while pi < f.paramCount { + if pi > 0 { QBE_Emit(qbe, ", "); } + let pty: String = QbeBackend_TypeWord(pnames[pi].typeKind); + QBE_Emit(qbe, pty); + pi = pi + 1; + } + QBE_EmitLine(qbe, ")"); +} + +// --------------------------------------------------------------------------- +// Generate complete QBE module +// --------------------------------------------------------------------------- + +func QbeBackend_Generate(mod: *HirModule) -> String { + let qbe: *QbeEmitter = bux_alloc(sizeof(QbeEmitter)) as *QbeEmitter; + qbe.dataSb = StringBuilder_NewCap(4096); + qbe.codeSb = StringBuilder_NewCap(32768); + qbe.tempId = 0; + qbe.labelId = 0; + qbe.strId = 0; + qbe.indent = 0; + qbe.inBlock = false; + qbe.constCount = mod.constCount; + qbe.consts = mod.consts; + + // Header + QBE_EmitLine(qbe, "# Generated by Bux QBE Backend"); + QBE_EmitLine(qbe, ""); + + // Struct types + if mod.structCount > 0 { + QBE_EmitLine(qbe, "# Struct types"); + var si: int = 0; + while si < mod.structCount { + QBE_EmitStructType(qbe, &mod.structs[si]); + si = si + 1; + } + QBE_EmitLine(qbe, ""); + } + + // QBE resolves extern functions at link time — no declarations needed + + // Functions + QBE_EmitLine(qbe, "# Functions"); + var i: int = 0; + while i < mod.funcCount { + QBE_EmitFunc(qbe, &mod.funcs[i]); + i = i + 1; + } + + // C entry point wrapper: main() -> Main() + QBE_EmitLine(qbe, "# C entry point"); + QBE_EmitLine(qbe, "export function w $main() {"); + QBE_EmitLine(qbe, "@start"); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "%r =w call $Main()"); + QBE_EmitLine(qbe, ""); + QBE_EmitIndent(qbe); + QBE_EmitLine(qbe, "ret %r"); + QBE_EmitLine(qbe, "}"); + + let result: String = QBE_Build(qbe); + return result; +} + +} // module QbeBackend diff --git a/src/c_backend.nim b/src/c_backend.nim index 411fe0a..1344b99 100644 --- a/src/c_backend.nim +++ b/src/c_backend.nim @@ -160,8 +160,11 @@ proc emitExpr(be: var CBackend, node: HirNode): string = else: return "false" of tkStringLiteral: var text = node.litToken.text + # Backtick raw string: strip backticks, escape content for C + if text.len >= 2 and text[0] == '`' and text[text.len-1] == '`': + text = "\"" & cEscape(text[1 ..< text.len-1]) & "\"" # If text has no surrounding quotes, it's from constFoldConstDecl (already unescaped) - if text.len >= 2 and text[0] == '"' and text[text.len-1] == '"': + elif text.len >= 2 and text[0] == '"' and text[text.len-1] == '"': # Strip c8" c16" c32" prefixes — in C they are just regular string literals if text.startsWith("c32\""): text = "\"" & cEscape(text[4 ..< text.len-1]) & "\"" diff --git a/src/lexer.nim b/src/lexer.nim index 17bf6cd..5ba66a7 100644 --- a/src/lexer.nim +++ b/src/lexer.nim @@ -277,6 +277,18 @@ proc scanString(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token discard lex.advance() # closing " result = lex.makeToken(tkStringLiteral, startLoc, startPos) +proc scanBacktickString(lex: var Lexer, startLoc: SourceLocation): Token = + ## Scan a backtick-delimited raw string literal: content is literal, + ## no escape processing, newlines are preserved. + let startPos = lex.pos - 1 # include the opening backtick + while not lex.isAtEnd() and lex.peek() != '`': + discard lex.advance() + if lex.isAtEnd(): + lex.emitError(startLoc, "unterminated backtick string literal") + else: + discard lex.advance() # closing backtick + result = lex.makeToken(tkStringLiteral, startLoc, startPos) + proc scanChar(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token = let startPos = lex.pos - prefixLen if lex.peek() == '\'': @@ -523,6 +535,11 @@ proc nextToken(lex: var Lexer): Token = if c == '"': return lex.scanString(startLoc, 0) + # Backtick-delimited raw string: `...` + if c == '`': + discard lex.advance() + return lex.scanBacktickString(startLoc) + # Char prefixes: c8' c16' c32' — must come before ident check if c == 'c' and lex.peek(1) in {'8', '1', '3'}: let d = lex.peek(1) diff --git a/src/token.nim b/src/token.nim index 44e4225..d4625d0 100644 --- a/src/token.nim +++ b/src/token.nim @@ -5,7 +5,7 @@ type ##Literals tkIntLiteral # 42 0xFF 0b1010 0o77 tkFloatLiteral # 3.14 1.0e-9 - tkStringLiteral # "hello" c8"hello" c16"hello" c32"hello" + tkStringLiteral # "hello" c8"hello" c16"hello" c32"hello" `raw multi-line` tkCharLiteral # 'A' c8'A' c16'A' c32'A' tkBoolLiteral # true false diff --git a/src_bux/cli.bux b/src_bux/cli.bux index 15e77c8..eb6e441 100644 --- a/src_bux/cli.bux +++ b/src_bux/cli.bux @@ -98,14 +98,14 @@ func Cli_Compile(source: String, sourceName: String) -> String { PrintInt(hirMod.funcCount); PrintLine(""); - // Phase 5: C code generation - let nimCode: String = NimBackend_Generate(hirMod); + // Phase 5: QBE SSA code generation + let qbeCode: String = QbeBackend_Generate(hirMod); // Cleanup Lexer_Free(lex); Sema_Free(sema); - return nimCode; + return qbeCode; } // --------------------------------------------------------------------------- @@ -123,17 +123,17 @@ func Cli_Build(srcPath: String, outPath: String) -> int { Print("Compiling "); PrintLine(srcPath); - let cCode: String = Cli_Compile(source, srcPath); + let ssaCode: String = Cli_Compile(source, srcPath); - if String_Eq(cCode, "") { + if String_Eq(ssaCode, "") { PrintLine("Compilation failed"); return 1; } - // Write C output - let ok: bool = WriteFile(outPath, cCode); + // Write SSA output + let ok: bool = WriteFile(outPath, ssaCode); if ok { - Print(" → Nim code written to "); + Print(" → SSA written to "); PrintLine(outPath); return 0; } else { @@ -233,6 +233,11 @@ func Cli_CompileSource(source: String, sourceName: String) -> *HirModule { } func Cli_BuildProject(projectDir: String) -> int { + let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml")); + var outName: String = man.name; + if String_Eq(outName, "") { + outName = "bux_out"; + } let srcDir: String = bux_path_join(projectDir, "src"); if !DirExists(srcDir) { Print("Error: no src/ directory in "); @@ -327,30 +332,79 @@ func Cli_BuildProject(projectDir: String) -> int { PrintLine("Lowering to HIR..."); let hirMod: *HirModule = HirLower_LowerModule(merged, sema); - // Nim code generation - PrintLine("Generating Nim code..."); - let nimCode: String = NimBackend_Generate(hirMod); + // QBE SSA code generation + PrintLine("Generating QBE SSA..."); + let qbeCode: String = QbeBackend_Generate(hirMod); // Create build directory let buildDir: String = bux_path_join(projectDir, "build"); discard bux_mkdir_if_needed(buildDir); - // Write Nim output - let nimFile: String = bux_path_join(buildDir, "main.nim"); - if !WriteFile(nimFile, nimCode) { - PrintLine("Error: cannot write main.nim"); + // Write SSA output + let ssaFile: String = bux_path_join(buildDir, "main.ssa"); + if !WriteFile(ssaFile, qbeCode) { + PrintLine("Error: cannot write main.ssa"); return 1; } - Print("Nim code written to "); - PrintLine(nimFile); + Print("SSA written to "); + PrintLine(ssaFile); - // Invoke Nim compiler - PrintLine("Invoking Nim compiler..."); - let outBin: String = bux_path_join(buildDir, "buxc3"); - let rc: int = bux_run_nim(nimFile, outBin); - if rc != 0 { - PrintLine("Error: Nim compilation failed"); + // Invoke QBE to generate assembly + PrintLine("Invoking QBE..."); + let asmFile: String = bux_path_join(buildDir, "main.s"); + var qbeCmd: String = bux_path_join(projectDir, "vendor/qbe/qbe"); + // Try relative to project first, then absolute + if !FileExists(qbeCmd) { + // Try from compiler location + qbeCmd = "../vendor/qbe/qbe"; + } + if !FileExists(qbeCmd) { + qbeCmd = "vendor/qbe/qbe"; + } + // Actually let's use bux_system with a constructed command + var cmdBuf: StringBuilder = StringBuilder_NewCap(256); + StringBuilder_Append(&cmdBuf, qbeCmd); + StringBuilder_Append(&cmdBuf, " -o "); + StringBuilder_Append(&cmdBuf, asmFile); + StringBuilder_Append(&cmdBuf, " "); + StringBuilder_Append(&cmdBuf, ssaFile); + let qbeRc: int = bux_system(StringBuilder_Build(&cmdBuf)); + if qbeRc != 0 { + PrintLine("Error: QBE compilation failed"); + return 1; + } + + // Link with cc + PrintLine("Linking..."); + var linkBuf: StringBuilder = StringBuilder_NewCap(512); + StringBuilder_Append(&linkBuf, "cc -o "); + let outBin: String = bux_path_join(buildDir, outName); + StringBuilder_Append(&linkBuf, outBin); + StringBuilder_Append(&linkBuf, " "); + StringBuilder_Append(&linkBuf, asmFile); + StringBuilder_Append(&linkBuf, " "); + // Find runtime.c and io.c + var rtPath: String = bux_path_join(projectDir, "stdlib/runtime.c"); + var ioPath: String = bux_path_join(projectDir, "stdlib/io.c"); + if !FileExists(rtPath) { + rtPath = bux_path_join(projectDir, "../stdlib/runtime.c"); + } + if !FileExists(ioPath) { + ioPath = bux_path_join(projectDir, "../stdlib/io.c"); + } + if FileExists(rtPath) { + StringBuilder_Append(&linkBuf, rtPath); + StringBuilder_Append(&linkBuf, " "); + } + if FileExists(ioPath) { + StringBuilder_Append(&linkBuf, ioPath); + StringBuilder_Append(&linkBuf, " "); + } + StringBuilder_Append(&linkBuf, "-lm"); + let linkRc: int = bux_system(StringBuilder_Build(&linkBuf)); + if linkRc != 0 { + PrintLine("Error: Linking failed"); return 1; } diff --git a/src_bux/hir_lower.bux b/src_bux/hir_lower.bux index 76b1b4e..07b2c2f 100644 --- a/src_bux/hir_lower.bux +++ b/src_bux/hir_lower.bux @@ -412,6 +412,8 @@ func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule { hm.funcs = ctx.funcs; hm.structCount = 0; hm.structs = bux_alloc(64 as uint * sizeof(HirStruct)) as *HirStruct; + hm.constCount = 0; + hm.consts = bux_alloc(512 as uint * sizeof(HirConst)) as *HirConst; // First pass: count structs (to allocate field arrays later) // Second pass: actually collect them @@ -468,6 +470,18 @@ func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule { } hm.structCount = hm.structCount + 1; } + if decl.kind == dkConst && hm.constCount < 512 { + let ci: int = hm.constCount; + hm.consts[ci].name = decl.strValue; + var val: int = 0; + if decl.constValue != null as *Expr { + if decl.constValue.kind == ekLiteral { + val = decl.constValue.intValue; + } + } + hm.consts[ci].value = val; + hm.constCount = hm.constCount + 1; + } if decl.kind == dkEnum { hm.enumCount = hm.enumCount + 1; } diff --git a/src_bux/lexer.bux b/src_bux/lexer.bux index 6bbfb2f..7fe64b1 100644 --- a/src_bux/lexer.bux +++ b/src_bux/lexer.bux @@ -384,6 +384,20 @@ func lexScanNumber(lex: *Lexer) { // Strings and chars // --------------------------------------------------------------------------- +func lexScanBacktickString(lex: *Lexer) { + lexMarkStart(lex); + if lexPeek(lex, 0) == 96 { discard lexAdvance(lex); } // opening backtick + while !lexIsAtEnd(lex) && lexPeek(lex, 0) != 96 { + discard lexAdvance(lex); + } + if lexIsAtEnd(lex) { + lexEmitDiag(lex, "unterminated backtick string literal"); + } else { + discard lexAdvance(lex); // closing backtick + } + lexEmitToken(lex, tkStringLiteral); +} + func lexScanString(lex: *Lexer) { lexMarkStart(lex); if lexPeek(lex, 0) == 34 { discard lexAdvance(lex); } // opening " @@ -618,6 +632,11 @@ func lexNextToken(lex: *Lexer) { lexScanString(lex); return; } + // Backtick raw string + if c == 96 { // backtick + lexScanBacktickString(lex); return; + } + // Char prefixes: c8' c16' c32' if c == 99 { // 'c' let d: uint32 = lexPeek(lex, 1); diff --git a/src_bux/parser.bux b/src_bux/parser.bux index 878367a..8ad0e5b 100644 --- a/src_bux/parser.bux +++ b/src_bux/parser.bux @@ -483,14 +483,33 @@ func parserIsBinaryOp(kind: int) -> bool { return false; } -func parserParseBinary(p: *Parser) -> *Expr { - var left: *Expr = parserParsePostfix(p); +func parserPrecedence(op: int) -> int { + if op == tkAssign || op == tkPlusAssign || op == tkMinusAssign || op == tkStarAssign || op == tkSlashAssign || op == tkPercentAssign || op == tkAmpAssign || op == tkPipeAssign || op == tkCaretAssign || op == tkShlAssign || op == tkShrAssign { return 1; } + if op == tkPipePipe { return 2; } + if op == tkAmpAmp { return 3; } + if op == tkPipe { return 4; } + if op == tkCaret { return 5; } + if op == tkAmp { return 6; } + if op == tkEq || op == tkNe { return 7; } + if op == tkLt || op == tkLe || op == tkGt || op == tkGe { return 8; } + if op == tkShl || op == tkShr { return 9; } + if op == tkPlus || op == tkMinus { return 10; } + if op == tkStar || op == tkSlash || op == tkPercent { return 11; } + if op == tkStarStar { return 12; } + return 0; +} - while parserIsBinaryOp(parserPeek(p, 0)) { +func parserParseBinaryPrec(p: *Parser, minPrec: int) -> *Expr { + var left: *Expr = parserParsePostfix(p); + while true { + let op: int = parserPeek(p, 0); + let prec: int = parserPrecedence(op); + if prec < minPrec { break; } let opTok: LexToken = parserAdvance(p); let line: uint32 = opTok.line; let col: uint32 = opTok.column; - let right: *Expr = parserParsePostfix(p); + let nextMinPrec: int = prec + 1; + let right: *Expr = parserParseBinaryPrec(p, nextMinPrec); let e: *Expr = parserMakeExpr(ekBinary, line, col); e.intValue = opTok.kind; e.child1 = left; @@ -500,6 +519,10 @@ func parserParseBinary(p: *Parser) -> *Expr { return left; } +func parserParseBinary(p: *Parser) -> *Expr { + return parserParseBinaryPrec(p, 1); +} + // --------------------------------------------------------------------------- // Ternary: cond ? then : else // --------------------------------------------------------------------------- diff --git a/src_bux/qbe_backend.bux b/src_bux/qbe_backend.bux new file mode 100644 index 0000000..8f4a029 --- /dev/null +++ b/src_bux/qbe_backend.bux @@ -0,0 +1,942 @@ +// qbe_backend.bux — QBE SSA IL generator backend +// Generates QBE SSA assembly from HIR. Replaces both C and Nim backends. +// Pipeline: HirModule → .ssa → qbe → .s → cc → executable + +import Std::String; +import Std::Io::{Print, PrintLine}; + +module QbeBackend { + +// --------------------------------------------------------------------------- +// QBE type mapping +// QBE base types: w (32b), l (64b), s (32f), d (64f), b (8b), h (16b) +// Aggregate: :Name = { type, type, ... } +// --------------------------------------------------------------------------- + +func QbeBackend_TypeBase(kind: int) -> String { + if kind == tyVoid { return ""; } + if kind == tyBool { return "w"; } + if kind == tyBool8 { return "b"; } + if kind == tyBool16 { return "h"; } + if kind == tyBool32 { return "w"; } + if kind == tyChar8 { return "b"; } + if kind == tyChar16 { return "h"; } + if kind == tyChar32 { return "w"; } + if kind == tyStr { return "l"; } + if kind == tyInt8 { return "b"; } + if kind == tyInt16 { return "h"; } + if kind == tyInt32 { return "w"; } + if kind == tyInt64 { return "l"; } + if kind == tyInt { return "l"; } + if kind == tyUInt8 { return "b"; } + if kind == tyUInt16 { return "h"; } + if kind == tyUInt32 { return "w"; } + if kind == tyUInt64 { return "l"; } + if kind == tyUInt { return "l"; } + if kind == tyFloat32 { return "s"; } + if kind == tyFloat64 { return "d"; } + if kind == tyPointer { return "l"; } + return "l"; +} + +// Return the "word" type for a given kind (used for comparisons, arithmetic) +func QbeBackend_TypeWord(kind: int) -> String { + if kind == tyFloat32 { return "s"; } + if kind == tyFloat64 { return "d"; } + if kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32 { return "w"; } + if kind == tyInt32 || kind == tyUInt32 || kind == tyChar32 { return "w"; } + if kind == tyChar8 { return "b"; } + if kind == tyChar16 { return "h"; } + return "l"; +} + +// --------------------------------------------------------------------------- +// Emitter state +// --------------------------------------------------------------------------- + +struct QbeEmitter { + dataSb: StringBuilder; + codeSb: StringBuilder; + tempId: int; + labelId: int; + strId: int; + indent: int; + // Track if we're currently in a block (after @label) + inBlock: bool; + // Const table for inline substitution + constCount: int; + consts: *HirConst; +} + +func QBE_Init() -> QbeEmitter { + var dsb: StringBuilder = StringBuilder_NewCap(4096); + var csb: StringBuilder = StringBuilder_NewCap(16384); + return QbeEmitter { dataSb: dsb, codeSb: csb, tempId: 0, labelId: 0, strId: 0, indent: 0, inBlock: false, constCount: 0, consts: null as *HirConst }; +} + +func QBE_FreshTemp(qbe: *QbeEmitter) -> String { + let id: int = qbe.tempId; + qbe.tempId = id + 1; + var prefix: String = "%t"; + return String_Concat(prefix, String_FromInt(id as int64)); +} + +func QBE_FreshLabel(qbe: *QbeEmitter) -> String { + let id: int = qbe.labelId; + qbe.labelId = id + 1; + var prefix: String = "@L"; + return String_Concat(prefix, String_FromInt(id as int64)); +} + +func QBE_FreshStr(qbe: *QbeEmitter) -> String { + let id: int = qbe.strId; + qbe.strId = id + 1; + var prefix: String = "$str_"; + return String_Concat(prefix, String_FromInt(id as int64)); +} + +func QBE_EmitData(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.dataSb, text); +} + +func QBE_EmitDataLine(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.dataSb, text); + StringBuilder_Append(&qbe.dataSb, "\n"); +} + +func QBE_EmitDataChar(qbe: *QbeEmitter, c: char8) { + var tmp: *char8 = bux_alloc(2) as *char8; + tmp[0] = c; + tmp[1] = 0 as char8; + StringBuilder_Append(&qbe.dataSb, tmp); +} + +func QBE_Emit(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.codeSb, text); +} + +func QBE_EmitLine(qbe: *QbeEmitter, text: String) { + StringBuilder_Append(&qbe.codeSb, text); + StringBuilder_Append(&qbe.codeSb, "\n"); +} + +func QBE_EmitChar(qbe: *QbeEmitter, c: char8) { + var tmp: *char8 = bux_alloc(2) as *char8; + tmp[0] = c; + tmp[1] = 0 as char8; + StringBuilder_Append(&qbe.codeSb, tmp); +} + +func QBE_EmitIndent(qbe: *QbeEmitter) { + var i: int = 0; + while i < qbe.indent { + StringBuilder_Append(&qbe.codeSb, " "); + i = i + 1; + } +} + +func QBE_Build(qbe: *QbeEmitter) -> String { + let dataPart: String = StringBuilder_Build(&qbe.dataSb); + let codePart: String = StringBuilder_Build(&qbe.codeSb); + var result: StringBuilder = StringBuilder_NewCap(2048); + StringBuilder_Append(&result, dataPart); + StringBuilder_Append(&result, codePart); + return StringBuilder_Build(&result); +} + +// --------------------------------------------------------------------------- +// Escape identifier for QBE +// QBE names: $global, %temp, @label +// --------------------------------------------------------------------------- + +func QBE_EscapeIdent(name: String) -> String { + if name == null as String || String_Eq(name, "") { return "_anon"; } + return name; +} + +func QBE_LocalName(name: String) -> String { + if name == null as String || String_Eq(name, "") { return "%_anon"; } + var prefix: String = "%"; + return String_Concat(prefix, name); +} + +// --------------------------------------------------------------------------- +// String literal → data section +// --------------------------------------------------------------------------- + +func QBE_EmitStringLiteral(qbe: *QbeEmitter, text: String) -> String { + let sym: String = QBE_FreshStr(qbe); + var content: String = text; + var start: int = 0; + var end: int = 0; + if content != null as String { + let slen: int = bux_strlen(content) as int; + end = slen; + if slen >= 2 { + // Regular string: "content" + if content[0] == 34 as char8 && content[slen - 1] == 34 as char8 { + start = 1; + end = slen - 1; + } + // Backtick raw string: `content` + if content[0] == 96 as char8 && content[slen - 1] == 96 as char8 { + start = 1; + end = slen - 1; + } + } + } + // Build data declaration into dataSb + // data $str = { b "content", b 0 } + QBE_EmitData(qbe, "data "); + QBE_EmitData(qbe, sym); + QBE_EmitData(qbe, " = { b \""); + var i: int = start; + while i < end { + let c: char8 = content[i]; + if c == 10 as char8 { QBE_EmitData(qbe, "\\n"); } + else if c == 13 as char8 { QBE_EmitData(qbe, "\\r"); } + else if c == 9 as char8 { QBE_EmitData(qbe, "\\t"); } + else if c == 34 as char8 { QBE_EmitData(qbe, "\\\""); } + else if c == 92 as char8 { QBE_EmitData(qbe, "\\\\"); } + else { QBE_EmitDataChar(qbe, c); } + i = i + 1; + } + QBE_EmitDataLine(qbe, "\", b 0 }"); + return sym; +} + +// --------------------------------------------------------------------------- +// Type definitions for structs +// --------------------------------------------------------------------------- + +func QBE_EmitStructType(qbe: *QbeEmitter, st: *HirStruct) { + QBE_Emit(qbe, "type :"); + QBE_Emit(qbe, QBE_EscapeIdent(st.name)); + QBE_Emit(qbe, " = { "); + var fi: int = 0; + while fi < st.fieldCount { + if fi > 0 { QBE_Emit(qbe, ", "); } + // Map field type; default to l if unknown + var ft: String = "l"; + if st.fields != null as *HirStructField { + let ftype: String = st.fields[fi].typeName; + if String_Eq(ftype, "int") || String_Eq(ftype, "int64") || String_Eq(ftype, "uint") || String_Eq(ftype, "uint64") || String_Eq(ftype, "String") || String_Eq(ftype, "*char8") { + ft = "l"; + } else if String_Eq(ftype, "int32") || String_Eq(ftype, "uint32") { + ft = "w"; + } else if String_Eq(ftype, "bool") || String_Eq(ftype, "char8") { + ft = "b"; + } else if String_Eq(ftype, "float64") { + ft = "d"; + } else if String_Eq(ftype, "float32") { + ft = "s"; + } + } + QBE_Emit(qbe, ft); + fi = fi + 1; + } + QBE_EmitLine(qbe, " }"); +} + +// --------------------------------------------------------------------------- +// Helper: check if a statement node is a block terminator +// --------------------------------------------------------------------------- + +func QBE_IsTerminator(node: *HirNode) -> bool { + if node == null as *HirNode { return false; } + if node.kind == hReturn { return true; } + if node.kind == hBreak { return true; } + if node.kind == hContinue { return true; } + if node.kind == hBlock { + var last: *HirNode = node.child1; + if last == null as *HirNode { return false; } + while last.child3 != null as *HirNode { + last = last.child3; + } + return QBE_IsTerminator(last); + } + return false; +} + +// --------------------------------------------------------------------------- +// Expression emission — returns the QBE value string (e.g. "%t3", "42", "$foo") +// For expressions that need multiple instructions, emits them inline. +// --------------------------------------------------------------------------- + +func QBE_EmitExpr(qbe: *QbeEmitter, node: *HirNode) -> String { + if node == null as *HirNode { + return "0"; + } + let kind: int = node.kind; + + // Literal + if kind == hLit { + if node.intValue == tkNull { + return "0"; + } + if node.intValue == tkStringLiteral { + return QBE_EmitStringLiteral(qbe, node.strValue); + } + if node.intValue == tkBoolLiteral { + if String_Eq(node.strValue, "true") { return "1"; } + return "0"; + } + // Numeric literal — emit as-is + return node.strValue; + } + + // Variable + if kind == hVar { + // Check if this is a const reference + var ci: int = 0; + while ci < qbe.constCount { + if String_Eq(qbe.consts[ci].name, node.strValue) { + var cv: String = String_FromInt(qbe.consts[ci].value as int64); + return cv; + } + ci = ci + 1; + } + let tmp: String = QBE_FreshTemp(qbe); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " load"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, QBE_LocalName(node.strValue)); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Assign (used as expression — e.g. inside another binary op) + if kind == hAssign { + var target: String = ""; + if node.child1 != null as *HirNode && node.child1.kind == hVar { + target = QBE_LocalName(node.child1.strValue); + } else { + target = QBE_EmitExpr(qbe, node.child1); + } + let val: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, target); + return val; + } + + // Binary operation + if kind == hBinary { + let op: int = node.intValue; + let left: String = QBE_EmitExpr(qbe, node.child1); + let right: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + + // Comparison operators + // QBE comparison ops: ceqw/ceql, cnew/cnel, csltw/csltl, cslew/cslel, csgtw/csgtl, csgew/csgel + var cmpOp: String = ""; + var cmpTy: String = ty; + if op == tkEq { cmpOp = "ceq"; } + else if op == tkNe { cmpOp = "cne"; } + else if op == tkLt { cmpOp = "cslt"; } + else if op == tkLe { cmpOp = "csle"; } + else if op == tkGt { cmpOp = "csgt"; } + else if op == tkGe { cmpOp = "csge"; } + + if !String_Eq(cmpOp, "") { + let cmpTmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, cmpTmp); + QBE_Emit(qbe, " =w "); + QBE_Emit(qbe, cmpOp); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, right); + QBE_EmitLine(qbe, ""); + // QBE comparisons produce word results; extend if needed + if !String_Eq(ty, "w") && !String_Eq(ty, "b") && !String_Eq(ty, "h") { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " extsw "); + QBE_Emit(qbe, cmpTmp); + QBE_EmitLine(qbe, ""); + return tmp; + } + return cmpTmp; + } + + // Arithmetic + var opName: String = ""; + if op == tkPlus { opName = "add"; } + else if op == tkMinus { opName = "sub"; } + else if op == tkStar { opName = "mul"; } + else if op == tkSlash { opName = "div"; } + else if op == tkPercent { opName = "rem"; } + else if op == tkAmp { opName = "and"; } + else if op == tkPipe { opName = "or"; } + else if op == tkCaret { opName = "xor"; } + else if op == tkShl { opName = "shl"; } + else if op == tkShr { opName = "shr"; } + + if !String_Eq(opName, "") { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, opName); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, right); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Logical and/or — convert to branch sequences + if op == tkAmpAmp { + let tmp: String = QBE_FreshTemp(qbe); + let lTrue: String = QBE_FreshLabel(qbe); + let lFalse: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lTrue); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lFalse); + QBE_EmitLine(qbe, lTrue); + let rVal: String = QBE_EmitExpr(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_EmitLine(qbe, " =w copy rVal"); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + QBE_EmitLine(qbe, lFalse); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_EmitLine(qbe, " =w copy 0"); + QBE_EmitLine(qbe, lEnd); + return tmp; + } + if op == tkPipePipe { + let tmp: String = QBE_FreshTemp(qbe); + let lTrue: String = QBE_FreshLabel(qbe); + let lFalse: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, left); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lTrue); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lFalse); + QBE_EmitLine(qbe, lTrue); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_EmitLine(qbe, " =w copy 1"); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + QBE_EmitLine(qbe, lFalse); + let rVal: String = QBE_EmitExpr(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " =w copy "); + QBE_EmitLine(qbe, rVal); + QBE_EmitLine(qbe, lEnd); + return tmp; + } + + return left; + } + + // Unary + if kind == hUnary { + let op: int = node.intValue; + let operand: String = QBE_EmitExpr(qbe, node.child1); + let ty: String = QbeBackend_TypeWord(node.typeKind); + + if op == tkBang || op == tkMinus { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + if op == tkBang { + // xor with 1 to invert boolean + QBE_Emit(qbe, " xor "); + QBE_Emit(qbe, operand); + QBE_Emit(qbe, ", 1"); + } else { + QBE_Emit(qbe, " sub 0, "); + QBE_Emit(qbe, operand); + } + QBE_EmitLine(qbe, ""); + return tmp; + } + if op == tkAmp { + // Address-of: just return the variable name (it's already a pointer) + // The operand should be a variable + if node.child1 != null as *HirNode && node.child1.kind == hVar { + return QBE_EscapeIdent(node.child1.strValue); + } + return operand; + } + return operand; + } + + // Call + if kind == hCall { + // Pre-evaluate arguments to avoid emitting instructions inside the call + let tmp: String = QBE_FreshTemp(qbe); + let retTy: String = QbeBackend_TypeWord(node.typeKind); + var arg1: String = ""; + var arg2: String = ""; + if node.child1 != null as *HirNode { + arg1 = QBE_EmitExpr(qbe, node.child1); + } + if node.child2 != null as *HirNode { + arg2 = QBE_EmitExpr(qbe, node.child2); + } + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, retTy); + QBE_Emit(qbe, " call $"); + QBE_Emit(qbe, QBE_EscapeIdent(node.strValue)); + QBE_Emit(qbe, "("); + if node.child1 != null as *HirNode { + let a1ty: String = QbeBackend_TypeWord(node.child1.typeKind); + QBE_Emit(qbe, a1ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, arg1); + } + if node.child2 != null as *HirNode { + let a2ty: String = QbeBackend_TypeWord(node.child2.typeKind); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, a2ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, arg2); + } + QBE_EmitLine(qbe, ")"); + return tmp; + } + + // Load (dereference) + if kind == hLoad { + let ptr: String = QBE_EmitExpr(qbe, node.child1); + let tmp: String = QBE_FreshTemp(qbe); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " load"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, ptr); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Field pointer: &obj.field + if kind == hFieldPtr { + let base: String = QBE_EmitExpr(qbe, node.child1); + let tmp: String = QBE_FreshTemp(qbe); + // add base, offset — need to know offset; for now assume 0 for first field + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " =l add "); + QBE_Emit(qbe, base); + QBE_Emit(qbe, ", 0"); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Cast + if kind == hCast { + let src: String = QBE_EmitExpr(qbe, node.child1); + let dstTy: String = QbeBackend_TypeWord(node.typeKind); + let srcTy: String = QbeBackend_TypeWord(node.child1.typeKind); + if String_Eq(dstTy, srcTy) { + return src; + } + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " ="); + QBE_Emit(qbe, dstTy); + // QBE cast instructions: extsw, extuw, extsl, extul, truncl, stosi, etc. + // For simplicity, use copy for now and let QBE handle it + QBE_Emit(qbe, " copy "); + QBE_Emit(qbe, src); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // SizeOf + if kind == hSizeOf { + let tmp: String = QBE_FreshTemp(qbe); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, tmp); + QBE_Emit(qbe, " =l copy 8"); + QBE_EmitLine(qbe, ""); + return tmp; + } + + // Fallback + return "0"; +} + +// --------------------------------------------------------------------------- +// Statement emission — no return value, just side effects +// --------------------------------------------------------------------------- + +func QBE_EmitStmt(qbe: *QbeEmitter, node: *HirNode) { + if node == null as *HirNode { return; } + let kind: int = node.kind; + + // Store: declaration or assignment + if kind == hStore { + if node.child1 != null as *HirNode && node.child1.kind == hAlloca { + // Declaration: alloca + init + let varName: String = QBE_LocalName(node.child1.strValue); + let varTy: String = QbeBackend_TypeWord(node.child1.typeKind); + // Emit alloca + QBE_EmitIndent(qbe); + QBE_Emit(qbe, varName); + QBE_Emit(qbe, " =l alloc4 8"); + QBE_EmitLine(qbe, ""); + if node.child2 != null as *HirNode { + let val: String = QBE_EmitExpr(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, varTy); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, varName); + } + return; + } + // Plain assignment + let target: String = QBE_EmitExpr(qbe, node.child1); + let val: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, target); + return; + } + + // Assign: same as Store without alloca + if kind == hAssign { + var target: String = ""; + if node.child1 != null as *HirNode && node.child1.kind == hVar { + target = QBE_LocalName(node.child1.strValue); + } else { + target = QBE_EmitExpr(qbe, node.child1); + } + let val: String = QBE_EmitExpr(qbe, node.child2); + let ty: String = QbeBackend_TypeWord(node.typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, ty); + QBE_Emit(qbe, " "); + QBE_Emit(qbe, val); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, target); + return; + } + + // Return + if kind == hReturn { + if node.child1 != null as *HirNode { + let val: String = QBE_EmitExpr(qbe, node.child1); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "ret "); + QBE_EmitLine(qbe, val); + } else { + QBE_EmitIndent(qbe); + QBE_EmitLine(qbe, "ret"); + } + return; + } + + // If + if kind == hIf { + let condVal: String = QBE_EmitExpr(qbe, node.child1); + let lThen: String = QBE_FreshLabel(qbe); + let lElse: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, condVal); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lThen); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lElse); + + QBE_EmitLine(qbe, lThen); + QBE_EmitStmt(qbe, node.child2); + if !QBE_IsTerminator(node.child2) { + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + } + + QBE_EmitLine(qbe, lElse); + QBE_EmitStmt(qbe, node.child3); + if !QBE_IsTerminator(node.child3) { + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lEnd); + } + + QBE_EmitLine(qbe, lEnd); + return; + } + + // While + if kind == hWhile { + let lCond: String = QBE_FreshLabel(qbe); + let lBody: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lCond); + + QBE_EmitLine(qbe, lCond); + let condVal: String = QBE_EmitExpr(qbe, node.child1); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jnz "); + QBE_Emit(qbe, condVal); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, lBody); + QBE_Emit(qbe, ", "); + QBE_EmitLine(qbe, lEnd); + + QBE_EmitLine(qbe, lBody); + QBE_EmitStmt(qbe, node.child2); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lCond); + + QBE_EmitLine(qbe, lEnd); + return; + } + + // Loop (infinite) + if kind == hLoop { + let lBody: String = QBE_FreshLabel(qbe); + let lEnd: String = QBE_FreshLabel(qbe); + + // Save loop labels for break/continue (not implemented yet) + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lBody); + + QBE_EmitLine(qbe, lBody); + QBE_EmitStmt(qbe, node.child1); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "jmp "); + QBE_EmitLine(qbe, lBody); + + QBE_EmitLine(qbe, lEnd); + return; + } + + // Break / Continue + if kind == hBreak { + // TODO: need loop label stack + return; + } + if kind == hContinue { + // TODO: need loop label stack + return; + } + + // Block — sequence of statements + if kind == hBlock { + var child: *HirNode = node.child1; + while child != null as *HirNode { + QBE_EmitStmt(qbe, child); + child = child.child3; + } + return; + } + + // Call as statement + if kind == hCall { + QBE_EmitExpr(qbe, node); + return; + } + + // Alloca as standalone statement + if kind == hAlloca { + // Already handled in Store; standalone alloca is a no-op + return; + } + + // Fallback: any other expression used as a statement + discard QBE_EmitExpr(qbe, node); +} + +// --------------------------------------------------------------------------- +// Function emission +// --------------------------------------------------------------------------- + +func QBE_EmitFunc(qbe: *QbeEmitter, f: *HirFunc) { + if f.body == null as *HirNode { return; } + + QBE_Emit(qbe, "export function "); + let retTy: String = QbeBackend_TypeWord(f.retTypeKind); + QBE_Emit(qbe, retTy); + QBE_Emit(qbe, " $"); + QBE_Emit(qbe, QBE_EscapeIdent(f.name)); + QBE_Emit(qbe, "("); + + // Parameters + var pi: int = 0; + var pnames: *HirParam = &f.param0; + while pi < f.paramCount { + if pi > 0 { QBE_Emit(qbe, ", "); } + let pty: String = QbeBackend_TypeWord(pnames[pi].typeKind); + QBE_Emit(qbe, pty); + QBE_Emit(qbe, " %_p_"); + QBE_Emit(qbe, QBE_EscapeIdent(pnames[pi].name)); + pi = pi + 1; + } + QBE_EmitLine(qbe, ") {"); + + // Entry block + QBE_EmitLine(qbe, "@start"); + + // Allocate local slots for parameters (QBE params are values, not pointers) + // Use _p_ prefix to avoid SSA name conflict with the local allocation + pi = 0; + pnames = &f.param0; + while pi < f.paramCount { + let pName: String = pnames[pi].name; + let pTy: String = QbeBackend_TypeWord(pnames[pi].typeKind); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, QBE_LocalName(pName)); + QBE_Emit(qbe, " =l alloc4 8"); + QBE_EmitLine(qbe, ""); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "store"); + QBE_Emit(qbe, pTy); + QBE_Emit(qbe, " %_p_"); + QBE_Emit(qbe, QBE_EscapeIdent(pName)); + QBE_Emit(qbe, ", "); + QBE_Emit(qbe, QBE_LocalName(pName)); + QBE_EmitLine(qbe, ""); + pi = pi + 1; + } + + // Body + QBE_EmitStmt(qbe, f.body); + + QBE_EmitLine(qbe, "}"); + QBE_EmitLine(qbe, ""); +} + +// --------------------------------------------------------------------------- +// Extern function declarations +// --------------------------------------------------------------------------- + +func QBE_EmitExtern(qbe: *QbeEmitter, f: *HirFunc) { + QBE_Emit(qbe, "function "); + let retTy: String = QbeBackend_TypeWord(f.retTypeKind); + QBE_Emit(qbe, retTy); + QBE_Emit(qbe, " $"); + QBE_Emit(qbe, QBE_EscapeIdent(f.name)); + QBE_Emit(qbe, "("); + var pi: int = 0; + var pnames: *HirParam = &f.param0; + while pi < f.paramCount { + if pi > 0 { QBE_Emit(qbe, ", "); } + let pty: String = QbeBackend_TypeWord(pnames[pi].typeKind); + QBE_Emit(qbe, pty); + pi = pi + 1; + } + QBE_EmitLine(qbe, ")"); +} + +// --------------------------------------------------------------------------- +// Generate complete QBE module +// --------------------------------------------------------------------------- + +func QbeBackend_Generate(mod: *HirModule) -> String { + let qbe: *QbeEmitter = bux_alloc(sizeof(QbeEmitter)) as *QbeEmitter; + qbe.dataSb = StringBuilder_NewCap(4096); + qbe.codeSb = StringBuilder_NewCap(32768); + qbe.tempId = 0; + qbe.labelId = 0; + qbe.strId = 0; + qbe.indent = 0; + qbe.inBlock = false; + qbe.constCount = mod.constCount; + qbe.consts = mod.consts; + + // Header + QBE_EmitLine(qbe, "# Generated by Bux QBE Backend"); + QBE_EmitLine(qbe, ""); + + // Struct types + if mod.structCount > 0 { + QBE_EmitLine(qbe, "# Struct types"); + var si: int = 0; + while si < mod.structCount { + QBE_EmitStructType(qbe, &mod.structs[si]); + si = si + 1; + } + QBE_EmitLine(qbe, ""); + } + + // QBE resolves extern functions at link time — no declarations needed + + // Functions + QBE_EmitLine(qbe, "# Functions"); + var i: int = 0; + while i < mod.funcCount { + QBE_EmitFunc(qbe, &mod.funcs[i]); + i = i + 1; + } + + // C entry point wrapper: main() -> Main() + QBE_EmitLine(qbe, "# C entry point"); + QBE_EmitLine(qbe, "export function w $main() {"); + QBE_EmitLine(qbe, "@start"); + QBE_EmitIndent(qbe); + QBE_Emit(qbe, "%r =w call $Main()"); + QBE_EmitLine(qbe, ""); + QBE_EmitIndent(qbe); + QBE_EmitLine(qbe, "ret %r"); + QBE_EmitLine(qbe, "}"); + + let result: String = QBE_Build(qbe); + return result; +} + +} // module QbeBackend diff --git a/stdlib/runtime.c b/stdlib/runtime.c index d61fff8..7547547 100644 --- a/stdlib/runtime.c +++ b/stdlib/runtime.c @@ -479,6 +479,7 @@ char* bux_read_file(const char* path) { long size = ftell(f); fseek(f, 0, SEEK_SET); char* buf = (char*)bux_alloc((size_t)size + 1); + if (!buf) { fclose(f); return NULL; } size_t read = fread(buf, 1, (size_t)size, f); fclose(f); buf[read] = '\0';