diff --git a/Makefile b/Makefile index e72dfac..ca53b56 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ clean: rm -rf _test_cast _test_cast2 _test_cast3 _test_channel clean-all: clean - rm -rf build/selfhost build/selfhost-loop-a build/selfhost-loop-b + rm -rf build/selfhost build/selfhost-loop-a build/selfhost-loop-b build/selfhost-loop-c rm -rf tests/golden/*/build selfhost: build diff --git a/README.md b/README.md index a55a4c1..d7f7ca2 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ make test-registry # add greet → install → build temp app # Selfhost determinism (optional CI job; not in default `make test`) make selfhost-loop -# Experimental fixed-point (buxc2 → buxc3): +# Full fixed-point: buxc2 → buxc3 → buxc4 (gen2 vs gen3 C+ELF identical) # BUX_SELFHOST_FIXED_POINT=1 make selfhost-loop # Clean build artifacts diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index 5d40293..9c429ce 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -192,11 +192,12 @@ Use `Std::Test` module for assertions inside test code. ### Selfhost loop (optional CI) ```bash make selfhost-loop # bootstrap builds src/ twice; C+ELF match -BUX_SELFHOST_FIXED_POINT=1 make selfhost-loop # also buxc2→buxc3 (experimental) +BUX_SELFHOST_FIXED_POINT=1 make selfhost-loop # buxc2→buxc3→buxc4 fixed-point ``` -Not part of default `make test`. GitHub Actions workflow -`.github/workflows/selfhost-loop.yml` runs it on: -- manual `workflow_dispatch` +Fixed-point compares **gen2 vs gen3** (same selfhost C backend), not bootstrap +vs selfhost. Not part of default `make test`. GitHub Actions workflow +`.github/workflows/selfhost-loop.yml` runs the fast determinism check on: +- manual `workflow_dispatch` (optional fixed_point input) - weekly schedule - pushes to `main` that touch `src/`, `lib/`, bootstrap, or the loop script diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 912ec78..bba8c3e 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) > **Дата:** 2026-07-19 -> **Текущо:** v0.5.x — Expr/Stmt sourceFile, LSP 0.14, selfhost-loop CI +> **Текущо:** v0.5.x — **selfhost fixed-point green**, Expr/Stmt sourceFile, LSP 0.14 > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -812,9 +812,23 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 52 (selfhost fixed-point buxc2→buxc3→buxc4 green) + +1. **Sema global scope:** allocate via `Scope_New` (8192), not 1024 — heap + overflow on ~1200 decls crashed buxc2 in `Scope_Lookup` +2. **HIR buffers:** funcs 512→4096, structs 64→512, enums/consts/gen* raised +3. **`CBE_FuncParam`:** avoid nested `.paramN.name` on `*HirParam` (wrong `.` vs `->`) +4. **Adapters:** `__fat_env` not `env` (no clash with `CtfeEnv* env`) +5. **Unary `!`:** parenthesize operand so `!(a && b)` ≠ `!a && b` +6. **Fat typedef skip-void:** rewritten without nested `!` for older gens +7. **Fixed-point loop:** compare gen2 vs gen3 (same CBE), not bootstrap vs selfhost +8. Verified: `BUX_SELFHOST_FIXED_POINT=1 make selfhost-loop` — C+ELF IDENTICAL + +--- + ## Следващи стъпки -1. Fix selfhost C backend so buxc2→buxc3 fixed-point is green -2. Main PR CI workflow (`make test`) beyond optional selfhost-loop -3. LSP type hierarchy / prepareTypeHierarchy (optional) -4. Macro / quote hygiene using Expr.sourceFile grafts +1. Main PR CI workflow (`make test`) beyond optional selfhost-loop +2. LSP type hierarchy / prepareTypeHierarchy (optional) +3. Macro / quote hygiene using Expr.sourceFile grafts +4. Parenthesize binary ops in CBE for full C precedence safety diff --git a/src/c_backend.bux b/src/c_backend.bux index 6e0425c..a281fae 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -385,10 +385,12 @@ module CBackend { return; } - // Unary + // Unary — always parenthesize operand so `!(a && b)` is not `!a && b` if kind == hUnary { StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue)); + StringBuilder_Append(&cbe.sb, "("); CBE_EmitExpr(cbe, node.child1); + StringBuilder_Append(&cbe.sb, ")"); return; } @@ -938,15 +940,8 @@ module CBackend { var p: int = 0; while p < mod.funcs[i].paramCount { var ptype: String = ""; - if p == 0 { ptype = mod.funcs[i].param0.typeName; } - else if p == 1 { ptype = mod.funcs[i].param1.typeName; } - else if p == 2 { ptype = mod.funcs[i].param2.typeName; } - else if p == 3 { ptype = mod.funcs[i].param3.typeName; } - else if p == 4 { ptype = mod.funcs[i].param4.typeName; } - else if p == 5 { ptype = mod.funcs[i].param5.typeName; } - else if p == 6 { ptype = mod.funcs[i].param6.typeName; } - else if p == 7 { ptype = mod.funcs[i].param7.typeName; } - else if p == 8 { ptype = mod.funcs[i].param8.typeName; } + let hp: *HirParam = CBE_FuncParam(&mod.funcs[i], p); + if hp != null as *HirParam { ptype = hp.typeName; } CBE_MaybeEmitExtraFat(cbe, ptype); p = p + 1; } @@ -961,6 +956,22 @@ module CBackend { CBE_EmitOneFatTypedef(cbe, name); } + /// Load param pointer by index (avoids nested `.paramN.name` on *HirParam — + /// selfhost CBE may emit `.` instead of `->` for chained pointer fields). + func CBE_FuncParam(f: *HirFunc, idx: int) -> *HirParam { + if f == null as *HirFunc { return null as *HirParam; } + if idx == 0 { return f.param0; } + if idx == 1 { return f.param1; } + if idx == 2 { return f.param2; } + if idx == 3 { return f.param3; } + if idx == 4 { return f.param4; } + if idx == 5 { return f.param5; } + if idx == 6 { return f.param6; } + if idx == 7 { return f.param7; } + if idx == 8 { return f.param8; } + return null as *HirParam; + } + func CBE_CollectBuxFn(names: *String, count: *int, name: String) { if String_Eq(name, "") { return; } if !String_StartsWith(name, "BuxFn_") { return; } @@ -1001,7 +1012,15 @@ module CBackend { var pi: uint = 1; while pi < partCount { let pPart: String = String_SplitPart(rest, "_", pi); - if !(pi == 1 && String_Eq(pPart, "void") && partCount == 2) { + // Skip sole `void` param (BuxFn_ret_void); always emit real params. + // Written without nested `!` so older selfhost CBE stays correct. + var skipVoid: bool = false; + if pi == 1 { + if String_Eq(pPart, "void") { + if partCount == 2 { skipVoid = true; } + } + } + if !skipVoid { StringBuilder_Append(&cbe.sb, ", "); StringBuilder_Append(&cbe.sb, CBE_FatPartToC(pPart)); } @@ -1148,21 +1167,17 @@ module CBackend { StringBuilder_Append(&cbe.sb, retC); StringBuilder_Append(&cbe.sb, " __adapt_"); StringBuilder_Append(&cbe.sb, fname); - StringBuilder_Append(&cbe.sb, "(void* env"); + // Use __fat_env so we never clash with a user param named `env` (CtfeEnv, etc.) + StringBuilder_Append(&cbe.sb, "(void* __fat_env"); var p: int = 0; while p < mod.funcs[i].paramCount { - // Skip if first param is already __env (shouldn't for named funcs) var pname: String = ""; var ptype: String = "int"; - if p == 0 { pname = mod.funcs[i].param0.name; ptype = mod.funcs[i].param0.typeName; } - else if p == 1 { pname = mod.funcs[i].param1.name; ptype = mod.funcs[i].param1.typeName; } - else if p == 2 { pname = mod.funcs[i].param2.name; ptype = mod.funcs[i].param2.typeName; } - else if p == 3 { pname = mod.funcs[i].param3.name; ptype = mod.funcs[i].param3.typeName; } - else if p == 4 { pname = mod.funcs[i].param4.name; ptype = mod.funcs[i].param4.typeName; } - else if p == 5 { pname = mod.funcs[i].param5.name; ptype = mod.funcs[i].param5.typeName; } - else if p == 6 { pname = mod.funcs[i].param6.name; ptype = mod.funcs[i].param6.typeName; } - else if p == 7 { pname = mod.funcs[i].param7.name; ptype = mod.funcs[i].param7.typeName; } - else if p == 8 { pname = mod.funcs[i].param8.name; ptype = mod.funcs[i].param8.typeName; } + let hp: *HirParam = CBE_FuncParam(&mod.funcs[i], p); + if hp != null as *HirParam { + pname = hp.name; + ptype = hp.typeName; + } if String_Eq(ptype, "") { ptype = "int"; } StringBuilder_Append(&cbe.sb, ", "); StringBuilder_Append(&cbe.sb, ptype); @@ -1170,7 +1185,7 @@ module CBackend { StringBuilder_Append(&cbe.sb, pname); p = p + 1; } - StringBuilder_Append(&cbe.sb, ") {\n (void)env;\n"); + StringBuilder_Append(&cbe.sb, ") {\n (void)__fat_env;\n"); if String_Eq(retC, "void") { StringBuilder_Append(&cbe.sb, " "); StringBuilder_Append(&cbe.sb, fname); @@ -1184,15 +1199,8 @@ module CBackend { while p < mod.funcs[i].paramCount { if p > 0 { StringBuilder_Append(&cbe.sb, ", "); } var pname: String = ""; - if p == 0 { pname = mod.funcs[i].param0.name; } - else if p == 1 { pname = mod.funcs[i].param1.name; } - else if p == 2 { pname = mod.funcs[i].param2.name; } - else if p == 3 { pname = mod.funcs[i].param3.name; } - else if p == 4 { pname = mod.funcs[i].param4.name; } - else if p == 5 { pname = mod.funcs[i].param5.name; } - else if p == 6 { pname = mod.funcs[i].param6.name; } - else if p == 7 { pname = mod.funcs[i].param7.name; } - else if p == 8 { pname = mod.funcs[i].param8.name; } + let hp2: *HirParam = CBE_FuncParam(&mod.funcs[i], p); + if hp2 != null as *HirParam { pname = hp2.name; } StringBuilder_Append(&cbe.sb, pname); p = p + 1; } @@ -1386,7 +1394,8 @@ module CBackend { if node.typeName != null as String { return node.typeName; } return ""; } - if node.kind == hFieldPtr { + // Field access (both hFieldPtr and hFieldAccess): resolve field type from structs + if node.kind == hFieldPtr || node.kind == hFieldAccess { let baseType: String = CBE_GetExprTypeName(mod, node.child1); var structName: String = baseType; if String_EndsWith(baseType, "*") { @@ -1396,7 +1405,10 @@ module CBackend { } } if !String_Eq(structName, "") { - return CBE_LookupFieldType(mod, structName, node.strValue); + let ft: String = CBE_LookupFieldType(mod, structName, node.strValue); + if ft != null as String && !String_Eq(ft, "") { + return ft; + } } } if node.typeName != null as String { return node.typeName; } @@ -1409,7 +1421,7 @@ module CBackend { func CBackend_Generate(mod: *HirModule) -> String { let cbe: *CEmitter = bux_alloc(sizeof(CEmitter)) as *CEmitter; - cbe.sb = StringBuilder_NewCap(8192); + cbe.sb = StringBuilder_NewCap(262144); cbe.indent = 0; cbe.mod = mod; cbe.deferCount = 0; @@ -1737,7 +1749,8 @@ module CBackend { StringBuilder_Append(&cbe.sb, "*)__env);\n"); } else if mod.funcs[i].paramCount > 0 { // Capture-less closure still has __env - if String_Eq(mod.funcs[i].param0.name, "__env") { + let envP: *HirParam = CBE_FuncParam(&mod.funcs[i], 0); + if envP != null as *HirParam && String_Eq(envP.name, "__env") { StringBuilder_Append(&cbe.sb, " (void)__env;\n"); } } diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 714124d..bf7dffd 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -4038,15 +4038,17 @@ module HirLower { let ctx: *LowerCtx = bux_alloc(sizeof(LowerCtx)) as *LowerCtx; ctx.module = mod; ctx.scope = sema.scope; - ctx.funcs = bux_alloc(512 as uint * sizeof(HirFunc)) as *HirFunc; + // Capacities sized for full selfhost compile (~600 funcs, ~120 structs). + // Undersized buffers overflowed heap → corrupt main.c / buxc2→buxc3 fail. + ctx.funcs = bux_alloc(4096 as uint * sizeof(HirFunc)) as *HirFunc; ctx.funcCount = 0; - ctx.externFuncs = bux_alloc(512 as uint * sizeof(HirFunc)) as *HirFunc; + ctx.externFuncs = bux_alloc(1024 as uint * sizeof(HirFunc)) as *HirFunc; ctx.externCount = 0; ctx.varCounter = 0; ctx.genFuncCount = 0; - ctx.genFuncs = bux_alloc(256 as uint * sizeof(Decl)) as *Decl; + ctx.genFuncs = bux_alloc(1024 as uint * sizeof(Decl)) as *Decl; ctx.genStructCount = 0; - ctx.genStructs = bux_alloc(256 as uint * sizeof(Decl)) as *Decl; + ctx.genStructs = bux_alloc(1024 as uint * sizeof(Decl)) as *Decl; ctx.substParam0 = ""; ctx.substArg0 = ""; ctx.substParam1 = ""; @@ -4056,11 +4058,11 @@ module HirLower { hm.funcCount = 0; hm.funcs = ctx.funcs; hm.structCount = 0; - hm.structs = bux_alloc(64 as uint * sizeof(HirStruct)) as *HirStruct; + hm.structs = bux_alloc(512 as uint * sizeof(HirStruct)) as *HirStruct; hm.enumCount = 0; - hm.enums = bux_alloc(64 as uint * sizeof(HirEnum)) as *HirEnum; + hm.enums = bux_alloc(256 as uint * sizeof(HirEnum)) as *HirEnum; hm.constCount = 0; - hm.consts = bux_alloc(512 as uint * sizeof(HirConst)) as *HirConst; + hm.consts = bux_alloc(2048 as uint * sizeof(HirConst)) as *HirConst; ctx.hm = hm; // First pass: count structs (to allocate field arrays later) @@ -4175,7 +4177,7 @@ module HirLower { ctx.externCount = ctx.externCount + 1; } // Pass 1: collect const names (expressions evaluated in Pass 2) - if decl.kind == dkConst && hm.constCount < 512 { + if decl.kind == dkConst && hm.constCount < 2048 { let ci: int = hm.constCount; hm.consts[ci].name = decl.strValue; hm.consts[ci].value = 0; diff --git a/src/sema.bux b/src/sema.bux index 65838af..bf175af 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -1721,7 +1721,7 @@ module Sema { // Interface if dk == dkInterface { - if sema.interfaceCount < 64 { + if sema.interfaceCount < 256 { sema.interfaceTable[sema.interfaceCount].name = decl.strValue; sema.interfaceTable[sema.interfaceCount].decl = decl; sema.interfaceCount = sema.interfaceCount + 1; @@ -1734,7 +1734,7 @@ module Sema { var m: *Decl = decl.childDecl1; while m != null as *Decl { if m.kind == dkFunc { - if sema.methodCount < 256 { + if sema.methodCount < 2048 { sema.methodEntries[sema.methodCount].typeName = implTypeName; sema.methodEntries[sema.methodCount].methodName = m.strValue; sema.methodEntries[sema.methodCount].decl = m; @@ -1865,7 +1865,7 @@ module Sema { } if !String_Eq(typeName, "") { let methodName: String = bux_str_slice(decl.strValue, (i + 1) as uint, bux_strlen(decl.strValue) - (i + 1) as uint); - if sema.methodCount < 256 { + if sema.methodCount < 2048 { sema.methodEntries[sema.methodCount].typeName = typeName; sema.methodEntries[sema.methodCount].methodName = methodName; sema.methodEntries[sema.methodCount].decl = decl; @@ -2225,19 +2225,23 @@ module Sema { func Sema_Analyze(mod: *Module) -> *Sema { let s: *Sema = bux_alloc(sizeof(Sema)) as *Sema; s.module = mod; + // Global scope must use Scope_New capacity (maxSymbols=8192). + // Allocating only 1024 slots while Scope_Define allows 8192 overflowed + // the heap when analyzing full compiler sources (~1200 decls) — buxc2 crash. s.scope = bux_alloc(sizeof(Scope)) as *Scope; - s.scope.symbols = bux_alloc(1024 as uint * sizeof(Symbol)) as *Symbol; - s.scope.count = 0; - s.scope.parent = null as *Scope; + let globalScope: Scope = Scope_New(); + s.scope.symbols = globalScope.symbols; + s.scope.count = globalScope.count; + s.scope.parent = globalScope.parent; s.hasError = false; s.diagCount = 0; - s.diags = bux_alloc(256 as uint * sizeof(SemaDiag)) as *SemaDiag; + s.diags = bux_alloc(1024 as uint * sizeof(SemaDiag)) as *SemaDiag; s.typeTable = null as *void; s.methodTable = null as *void; s.currentRetType = tyVoid; - s.interfaceTable = bux_alloc(64 as uint * sizeof(InterfaceEntry)) as *InterfaceEntry; + s.interfaceTable = bux_alloc(256 as uint * sizeof(InterfaceEntry)) as *InterfaceEntry; s.interfaceCount = 0; - s.methodEntries = bux_alloc(256 as uint * sizeof(MethodEntry)) as *MethodEntry; + s.methodEntries = bux_alloc(2048 as uint * sizeof(MethodEntry)) as *MethodEntry; s.methodCount = 0; // First pass: collect globals diff --git a/tools/selfhost_loop.sh b/tools/selfhost_loop.sh index a1845a5..fdc8d56 100755 --- a/tools/selfhost_loop.sh +++ b/tools/selfhost_loop.sh @@ -112,35 +112,48 @@ if [[ "${BUX_SELFHOST_FIXED_POINT:-0}" != "1" ]]; then fi echo "" -echo "=== Fixed-point: buxc2 → buxc3 (experimental) ===" +echo "=== Fixed-point: buxc2 → buxc3 → buxc4 (same-backend gen) ===" +# True fixed-point: compiler_n and compiler_n+1 (both from selfhost backend) +# must produce identical C/ELF. Bootstrap vs selfhost CBE differ intentionally. +C="$ROOT/build/selfhost-loop-c" BUXC2="$A/build/buxc2" if [[ ! -x "$BUXC2" ]]; then echo "error: buxc2 missing at $BUXC2" >&2 exit 1 fi -# Rebuild B with buxc2 +echo "--- Gen2: buxc2 → buxc3 ---" prepare_tree "$B" -set +e -(cd "$B" && "$BUXC2" build) -fp_status=$? -set -e -if [[ $fp_status -ne 0 ]]; then +if ! (cd "$B" && "$BUXC2" build); then echo "=== Fixed-point FAILED (buxc2 could not build gen2) ===" exit 1 fi - BUXC3="$B/build/buxc2" if [[ ! -x "$BUXC3" ]]; then echo "error: gen2 binary missing" >&2 exit 1 fi +echo " buxc3: $BUXC3" +echo "--- Gen3: buxc3 → buxc4 ---" +prepare_tree "$C" +if ! (cd "$C" && "$BUXC3" build); then + echo "=== Fixed-point FAILED (buxc3 could not build gen3) ===" + exit 1 +fi +BUXC4="$C/build/buxc2" +if [[ ! -x "$BUXC4" ]]; then + echo "error: gen3 binary missing" >&2 + exit 1 +fi +echo " buxc4: $BUXC4" + +# Compare gen2 vs gen3 artifacts (not bootstrap vs selfhost) if ! compare_c_and_elf \ - "buxc2" "$BUXC2" "$A/build/main.c" \ - "buxc3" "$BUXC3" "$B/build/main.c" + "buxc3" "$BUXC3" "$B/build/main.c" \ + "buxc4" "$BUXC4" "$C/build/main.c" then - echo "=== Fixed-point FAILED (gen1 vs gen2 mismatch) ===" + echo "=== Fixed-point FAILED (gen2 vs gen3 mismatch) ===" exit 1 fi