diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 8d4512d..69553d4 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 — multi-file #line, selfhost CI, **LSP 0.11 interface hierarchy**, Nexus KA +> **Текущо:** v0.5.x — HirNode sourceFile #line, selfhost CI, **LSP 0.12 path rename**, Nexus KA > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -740,9 +740,23 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 47 (HirNode-level sourceFile / mid-function #line) + +1. **`HirNode.sourceFile`** — per-statement path for `#line` (rare multi-file spans) +2. **`Lcx_StampSourceFile`**: after lowering a func/closure body, fill empty + node paths from `Decl.sourceFile` / `HirFunc.sourceFile` (keeps pre-set paths) +3. **`LowerCtx.currentSourceFile`** + closures inherit enclosing file +4. **C backend:** `lastDebugFile` + prefer `node.sourceFile` over func + `currentFile`; re-emit `#line` when **line or file** changes mid-function +5. `BUX_DEBUG_FILE` truly forces one path (no longer overwritten by per-func) +6. Smoke: `tools/smoke_selfhost.sh` — Util_Double body `#line` → Util.bux only +7. `make test-selfhost-smoke` / rebuild selfhost + +--- + ## Следващи стъпки -1. HirNode-level file for statements spanning multiple files (rare) -2. Optional: selfhost-loop as optional CI job (slow) -3. LSP find-implementations request (dedicated, beyond call hierarchy) -4. Workspace-wide import path index without open documents (optional polish) +1. Optional: selfhost-loop as optional CI job (slow) +2. LSP find-implementations request (dedicated, beyond call hierarchy) +3. Workspace-wide import path index without open documents (optional polish) +4. Expr/Stmt-level sourceFile if macros / cross-file inlining land diff --git a/src/c_backend.bux b/src/c_backend.bux index 196293c..6e0425c 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -80,6 +80,7 @@ module CBackend { currentRetType: String, // #line debug maps (E.4 selfhost parity) lastDebugLine: int, + lastDebugFile: String, // re-emit when file changes mid-function (HirNode.sourceFile) emitDebugLines: bool, currentFile: String, } @@ -140,14 +141,24 @@ module CBackend { if node == null as *HirNode { return; } if node.line == 0 { return; } let ln: int = node.line as int; - if ln == cbe.lastDebugLine { return; } + // Prefer per-node sourceFile (multi-file spans); else function-level currentFile + var file: String = cbe.currentFile; + if node.sourceFile != null as String && !String_Eq(node.sourceFile, "") { + file = node.sourceFile; + } + if file == null as String { file = ""; } + // Skip only when both line and file unchanged + if ln == cbe.lastDebugLine && String_Eq(file, cbe.lastDebugFile) { + return; + } cbe.lastDebugLine = ln; + cbe.lastDebugFile = file; // #line must start at column 0 StringBuilder_Append(&cbe.sb, "#line "); StringBuilder_AppendInt(&cbe.sb, ln as int64); - if !String_Eq(cbe.currentFile, "") { + if !String_Eq(file, "") { StringBuilder_Append(&cbe.sb, " \""); - StringBuilder_Append(&cbe.sb, cbe.currentFile); + StringBuilder_Append(&cbe.sb, file); StringBuilder_Append(&cbe.sb, "\""); } StringBuilder_Append(&cbe.sb, "\n"); @@ -1405,6 +1416,7 @@ module CBackend { cbe.movedCount = 0; cbe.tmpCounter = 0; cbe.lastDebugLine = 0; + cbe.lastDebugFile = ""; cbe.emitDebugLines = true; cbe.currentFile = ""; // Optional override for all #line paths (default: per-func Decl.sourceFile) @@ -1697,12 +1709,17 @@ module CBackend { continue; } // Per-function source path for multi-file #line (overrides BUX_DEBUG_FILE only if set) - if !String_Eq(mod.funcs[i].sourceFile, "") { - cbe.currentFile = mod.funcs[i].sourceFile; + // BUX_DEBUG_FILE forces one path for the whole unit; skip per-func when set. + let forcedDbg: String = bux_getenv("BUX_DEBUG_FILE"); + if forcedDbg == null as String || String_Eq(forcedDbg, "") { + if mod.funcs[i].sourceFile != null as String && !String_Eq(mod.funcs[i].sourceFile, "") { + cbe.currentFile = mod.funcs[i].sourceFile; + } } cbe.lastDebugLine = 0; + cbe.lastDebugFile = ""; // #line before the function itself - if cbe.emitDebugLines && !String_Eq(cbe.currentFile, "") { + if cbe.emitDebugLines && cbe.currentFile != null as String && !String_Eq(cbe.currentFile, "") { StringBuilder_Append(&cbe.sb, "#line 1 \""); StringBuilder_Append(&cbe.sb, cbe.currentFile); StringBuilder_Append(&cbe.sb, "\"\n"); diff --git a/src/hir.bux b/src/hir.bux index 6ea38fb..e770355 100644 --- a/src/hir.bux +++ b/src/hir.bux @@ -66,6 +66,9 @@ module Hir { // Extra data pointer (for children arrays, field lists, etc.) extraData: *void; extraCount: int; + // Per-node source path for #line (multi-file / rare mid-function spans) + // Empty → C backend falls back to HirFunc.sourceFile / currentFile + sourceFile: String; } // --------------------------------------------------------------------------- @@ -182,7 +185,7 @@ module Hir { typeKind: 0, typeName: "", strValue: "", intValue: 0, boolValue: false, child1: null as *HirNode, child2: null as *HirNode, child3: null as *HirNode, - extraData: null as *void, extraCount: 0 }; + extraData: null as *void, extraCount: 0, sourceFile: "" }; } func Hir_MakeLit(tokKind: int, tokText: String, line: uint32, col: uint32) -> HirNode { diff --git a/src/hir_lower.bux b/src/hir_lower.bux index e068c17..77296cb 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -52,6 +52,33 @@ module HirLower { patMapTo6: String, patMapFrom7: String, patMapTo7: String, + // Active source path for #line stamping (from Decl.sourceFile) + currentSourceFile: String, + } + + /// Fill empty HirNode.sourceFile from `file` (does not overwrite set paths). + /// Walks child1/2/3, call arg lists, and if-else via extraData. + func Lcx_StampSourceFile(node: *HirNode, file: String) { + if node == null as *HirNode { return; } + if file == null as String || String_Eq(file, "") { return; } + if node.sourceFile == null as String || String_Eq(node.sourceFile, "") { + node.sourceFile = file; + } + Lcx_StampSourceFile(node.child1, file); + Lcx_StampSourceFile(node.child2, file); + Lcx_StampSourceFile(node.child3, file); + // hCall / multi-arg: linked HirArgList in extraData + if (node.kind == hCall || node.kind == hCallIndirect) && node.extraData != null as *void { + var cur: *HirArgList = node.extraData as *HirArgList; + while cur != null as *HirArgList { + Lcx_StampSourceFile(cur.node, file); + cur = cur.next; + } + } + // hIf else branch may live in extraData (*HirNode) + if node.kind == hIf && node.extraData != null as *void { + Lcx_StampSourceFile(node.extraData as *HirNode, file); + } } func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String { @@ -3424,6 +3451,8 @@ module HirLower { f.isPublic = decl.isPublic; f.checkedFunc = ctx.checkedFunc; f.sourceFile = decl.sourceFile; + let oldSourceFile: String = ctx.currentSourceFile; + ctx.currentSourceFile = decl.sourceFile; f.paramCount = decl.paramCount; f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param0, &decl.param0, ctx); @@ -3507,12 +3536,16 @@ module HirLower { ctx.scope = &funcScope; if decl.refBody != null as *Block { f.body = Lcx_LowerBlock(ctx, decl.refBody, f.retTypeKind); + // Stamp every node with this function's source path (nodes may already + // carry a different file for rare multi-file spans; those are kept). + Lcx_StampSourceFile(f.body, f.sourceFile); } else { f.body = null as *HirNode; } ctx.scope = prevScope; ctx.checkedFunc = oldChecked; ctx.releaseFunc = oldRelease; + ctx.currentSourceFile = oldSourceFile; return f; } @@ -3528,6 +3561,8 @@ module HirLower { let numStr: String = String_FromInt(ctx.funcCount); f.name = String_Concat("__closure_", numStr); f.isPublic = false; + // Closures inherit the enclosing function's source path + f.sourceFile = ctx.currentSourceFile; let params: *Decl = expr.closureParams; // Fat-func ABI: leading void* __env, then user params @@ -3641,6 +3676,7 @@ module HirLower { ctx.envInstanceName = envInstanceName; if expr.refBlock != null as *Block { f.body = Lcx_LowerBlock(ctx, expr.refBlock, -1); + Lcx_StampSourceFile(f.body, f.sourceFile); } else { f.body = null as *HirNode; } diff --git a/tools/smoke_selfhost.sh b/tools/smoke_selfhost.sh index b7acaa4..10ea32d 100755 --- a/tools/smoke_selfhost.sh +++ b/tools/smoke_selfhost.sh @@ -108,4 +108,34 @@ if ! grep -qE '#line [0-9]+ ".*lib/.*\.bux"' "$MAIN_C"; then fi echo " multi-file #line: PASS (Util + Main + lib)" -echo "PASS: selfhost smoke (move_field + multi-file #line)" +# --------------------------------------------------------------------------- +# 3) HirNode-level sourceFile — statement #line inside Util_Double uses Util.bux +# (not only the function prolog), and never Main.bux inside that body. +# --------------------------------------------------------------------------- +echo "=== selfhost: HirNode sourceFile (#line in body) ===" +# Function definition (not the forward decl): #line 1 "…Util.bux" then int Util_Double( +util_body=$(awk ' + /#line 1 ".*Util\.bux"/ { grab=1 } + grab { print } + grab && /^}/ { exit } +' "$MAIN_C") +if [[ -z "$util_body" ]]; then + echo "error: could not find Util_Double definition block" >&2 + grep -n 'Util_Double\|Util\.bux' "$MAIN_C" | head -20 + exit 1 +fi +# Statement-level #line inside body (not only prolog #line 1) must point at Util.bux +if ! echo "$util_body" | grep -vE '#line 1 "' | grep -qE '#line [0-9]+ ".*Util\.bux"'; then + echo "error: Util_Double body has no statement #line …Util.bux" >&2 + echo "$util_body" + exit 1 +fi +# Body of Util_Double must not claim Main.bux +if echo "$util_body" | grep -qE '#line [0-9]+ ".*Main\.bux"'; then + echo "error: Util_Double body has #line Main.bux (wrong sourceFile)" >&2 + echo "$util_body" | grep -E '#line ' + exit 1 +fi +echo " HirNode sourceFile: PASS (Util_Double stmts → Util.bux)" + +echo "PASS: selfhost smoke (move_field + multi-file #line + HirNode sourceFile)"