feat(selfhost): HirNode.sourceFile for mid-function #line maps

Stamp each HIR node with Decl source path; C backend tracks lastDebugFile
so #line re-emits when file or line changes. Closures inherit the
enclosing function path. Smoke checks Util_Double body maps to Util.bux.
This commit is contained in:
2026-07-19 23:40:10 +03:00
parent 200adf4d68
commit c82757d51c
5 changed files with 113 additions and 13 deletions
+19 -5
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-19 > **Дата:** 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. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден 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) 1. Optional: selfhost-loop as optional CI job (slow)
2. Optional: selfhost-loop as optional CI job (slow) 2. LSP find-implementations request (dedicated, beyond call hierarchy)
3. LSP find-implementations request (dedicated, beyond call hierarchy) 3. Workspace-wide import path index without open documents (optional polish)
4. Workspace-wide import path index without open documents (optional polish) 4. Expr/Stmt-level sourceFile if macros / cross-file inlining land
+23 -6
View File
@@ -80,6 +80,7 @@ module CBackend {
currentRetType: String, currentRetType: String,
// #line debug maps (E.4 selfhost parity) // #line debug maps (E.4 selfhost parity)
lastDebugLine: int, lastDebugLine: int,
lastDebugFile: String, // re-emit when file changes mid-function (HirNode.sourceFile)
emitDebugLines: bool, emitDebugLines: bool,
currentFile: String, currentFile: String,
} }
@@ -140,14 +141,24 @@ module CBackend {
if node == null as *HirNode { return; } if node == null as *HirNode { return; }
if node.line == 0 { return; } if node.line == 0 { return; }
let ln: int = node.line as int; 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.lastDebugLine = ln;
cbe.lastDebugFile = file;
// #line must start at column 0 // #line must start at column 0
StringBuilder_Append(&cbe.sb, "#line "); StringBuilder_Append(&cbe.sb, "#line ");
StringBuilder_AppendInt(&cbe.sb, ln as int64); StringBuilder_AppendInt(&cbe.sb, ln as int64);
if !String_Eq(cbe.currentFile, "") { if !String_Eq(file, "") {
StringBuilder_Append(&cbe.sb, " \""); StringBuilder_Append(&cbe.sb, " \"");
StringBuilder_Append(&cbe.sb, cbe.currentFile); StringBuilder_Append(&cbe.sb, file);
StringBuilder_Append(&cbe.sb, "\""); StringBuilder_Append(&cbe.sb, "\"");
} }
StringBuilder_Append(&cbe.sb, "\n"); StringBuilder_Append(&cbe.sb, "\n");
@@ -1405,6 +1416,7 @@ module CBackend {
cbe.movedCount = 0; cbe.movedCount = 0;
cbe.tmpCounter = 0; cbe.tmpCounter = 0;
cbe.lastDebugLine = 0; cbe.lastDebugLine = 0;
cbe.lastDebugFile = "";
cbe.emitDebugLines = true; cbe.emitDebugLines = true;
cbe.currentFile = ""; cbe.currentFile = "";
// Optional override for all #line paths (default: per-func Decl.sourceFile) // Optional override for all #line paths (default: per-func Decl.sourceFile)
@@ -1697,12 +1709,17 @@ module CBackend {
continue; continue;
} }
// Per-function source path for multi-file #line (overrides BUX_DEBUG_FILE only if set) // Per-function source path for multi-file #line (overrides BUX_DEBUG_FILE only if set)
if !String_Eq(mod.funcs[i].sourceFile, "") { // BUX_DEBUG_FILE forces one path for the whole unit; skip per-func when set.
cbe.currentFile = mod.funcs[i].sourceFile; 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.lastDebugLine = 0;
cbe.lastDebugFile = "";
// #line before the function itself // #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, "#line 1 \"");
StringBuilder_Append(&cbe.sb, cbe.currentFile); StringBuilder_Append(&cbe.sb, cbe.currentFile);
StringBuilder_Append(&cbe.sb, "\"\n"); StringBuilder_Append(&cbe.sb, "\"\n");
+4 -1
View File
@@ -66,6 +66,9 @@ module Hir {
// Extra data pointer (for children arrays, field lists, etc.) // Extra data pointer (for children arrays, field lists, etc.)
extraData: *void; extraData: *void;
extraCount: int; 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: "", typeKind: 0, typeName: "",
strValue: "", intValue: 0, boolValue: false, strValue: "", intValue: 0, boolValue: false,
child1: null as *HirNode, child2: null as *HirNode, child3: null as *HirNode, 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 { func Hir_MakeLit(tokKind: int, tokText: String, line: uint32, col: uint32) -> HirNode {
+36
View File
@@ -52,6 +52,33 @@ module HirLower {
patMapTo6: String, patMapTo6: String,
patMapFrom7: String, patMapFrom7: String,
patMapTo7: 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 { func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String {
@@ -3424,6 +3451,8 @@ module HirLower {
f.isPublic = decl.isPublic; f.isPublic = decl.isPublic;
f.checkedFunc = ctx.checkedFunc; f.checkedFunc = ctx.checkedFunc;
f.sourceFile = decl.sourceFile; f.sourceFile = decl.sourceFile;
let oldSourceFile: String = ctx.currentSourceFile;
ctx.currentSourceFile = decl.sourceFile;
f.paramCount = decl.paramCount; f.paramCount = decl.paramCount;
f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam; f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam;
Lcx_LowerParam(f.param0, &decl.param0, ctx); Lcx_LowerParam(f.param0, &decl.param0, ctx);
@@ -3507,12 +3536,16 @@ module HirLower {
ctx.scope = &funcScope; ctx.scope = &funcScope;
if decl.refBody != null as *Block { if decl.refBody != null as *Block {
f.body = Lcx_LowerBlock(ctx, decl.refBody, f.retTypeKind); 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 { } else {
f.body = null as *HirNode; f.body = null as *HirNode;
} }
ctx.scope = prevScope; ctx.scope = prevScope;
ctx.checkedFunc = oldChecked; ctx.checkedFunc = oldChecked;
ctx.releaseFunc = oldRelease; ctx.releaseFunc = oldRelease;
ctx.currentSourceFile = oldSourceFile;
return f; return f;
} }
@@ -3528,6 +3561,8 @@ module HirLower {
let numStr: String = String_FromInt(ctx.funcCount); let numStr: String = String_FromInt(ctx.funcCount);
f.name = String_Concat("__closure_", numStr); f.name = String_Concat("__closure_", numStr);
f.isPublic = false; f.isPublic = false;
// Closures inherit the enclosing function's source path
f.sourceFile = ctx.currentSourceFile;
let params: *Decl = expr.closureParams; let params: *Decl = expr.closureParams;
// Fat-func ABI: leading void* __env, then user params // Fat-func ABI: leading void* __env, then user params
@@ -3641,6 +3676,7 @@ module HirLower {
ctx.envInstanceName = envInstanceName; ctx.envInstanceName = envInstanceName;
if expr.refBlock != null as *Block { if expr.refBlock != null as *Block {
f.body = Lcx_LowerBlock(ctx, expr.refBlock, -1); f.body = Lcx_LowerBlock(ctx, expr.refBlock, -1);
Lcx_StampSourceFile(f.body, f.sourceFile);
} else { } else {
f.body = null as *HirNode; f.body = null as *HirNode;
} }
+31 -1
View File
@@ -108,4 +108,34 @@ if ! grep -qE '#line [0-9]+ ".*lib/.*\.bux"' "$MAIN_C"; then
fi fi
echo " multi-file #line: PASS (Util + Main + lib)" 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)"