feat(selfhost): Expr/Stmt/Block sourceFile for multi-file and macros
Stamp AST trees from Decl path; lower prefers node sourceFile over func-level path so grafted macro nodes keep their origin for #line maps.
This commit is contained in:
+18
-5
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 2026-07-19
|
||||||
> **Текущо:** v0.5.x — **LSP 0.14 workspace imports**, implementation, selfhost-loop CI
|
> **Текущо:** v0.5.x — Expr/Stmt sourceFile, LSP 0.14, selfhost-loop CI
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -799,9 +799,22 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Сесия 51 (Expr/Stmt sourceFile for multi-file / macros)
|
||||||
|
|
||||||
|
1. **`Expr.sourceFile` / `Stmt.sourceFile` / `Block.sourceFile`**
|
||||||
|
2. **`Ast_StampExprFile` / `Ast_StampStmtFile` / `Ast_StampBlockFile` /
|
||||||
|
`Ast_StampPatternFile`** — fill empty slots only (grafted nodes keep path)
|
||||||
|
3. **`Cli_StampSourceFile`** stamps decl body + const init + default params
|
||||||
|
4. **Lower:** `Lcx_SetNodeLoc` / `Lcx_SourceFileFor` prefer AST file, else
|
||||||
|
`currentSourceFile`; `Lcx_StampSourceFile` still fills empties at func end
|
||||||
|
5. Smoke: Main body `#line` → Main.bux only (no Util leak); Util isolation kept
|
||||||
|
6. Rebuild selfhost; `make test-selfhost-smoke`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Следващи стъпки
|
## Следващи стъпки
|
||||||
|
|
||||||
1. Expr/Stmt-level sourceFile if macros / cross-file inlining land
|
1. Fix selfhost C backend so buxc2→buxc3 fixed-point is green
|
||||||
2. Fix selfhost C backend so buxc2→buxc3 fixed-point is green
|
2. Main PR CI workflow (`make test`) beyond optional selfhost-loop
|
||||||
3. Main PR CI workflow (`make test`) beyond optional selfhost-loop
|
3. LSP type hierarchy / prepareTypeHierarchy (optional)
|
||||||
4. LSP type hierarchy / prepareTypeHierarchy (optional)
|
4. Macro / quote hygiene using Expr.sourceFile grafts
|
||||||
|
|||||||
+102
-2
@@ -138,6 +138,8 @@ module Ast {
|
|||||||
kind: int,
|
kind: int,
|
||||||
line: uint32,
|
line: uint32,
|
||||||
column: uint32,
|
column: uint32,
|
||||||
|
// Source file for multi-file #line / future macros (empty = inherit Decl)
|
||||||
|
sourceFile: String,
|
||||||
// Common fields
|
// Common fields
|
||||||
strValue: String, // ident name, path segments, field name, callee
|
strValue: String, // ident name, path segments, field name, callee
|
||||||
intValue: int, // operator kind, intrinsic kind
|
intValue: int, // operator kind, intrinsic kind
|
||||||
@@ -193,6 +195,7 @@ module Ast {
|
|||||||
struct Block {
|
struct Block {
|
||||||
line: uint32,
|
line: uint32,
|
||||||
column: uint32,
|
column: uint32,
|
||||||
|
sourceFile: String, // multi-file #line (empty = inherit Decl)
|
||||||
stmtCount: int,
|
stmtCount: int,
|
||||||
firstStmt: *Stmt,
|
firstStmt: *Stmt,
|
||||||
lastStmt: *Stmt,
|
lastStmt: *Stmt,
|
||||||
@@ -227,6 +230,8 @@ module Ast {
|
|||||||
kind: int,
|
kind: int,
|
||||||
line: uint32,
|
line: uint32,
|
||||||
column: uint32,
|
column: uint32,
|
||||||
|
// Source file for multi-file #line / future macros (empty = inherit Decl)
|
||||||
|
sourceFile: String,
|
||||||
// Common fields
|
// Common fields
|
||||||
strValue: String, // let name, pattern ident, label, for var
|
strValue: String, // let name, pattern ident, label, for var
|
||||||
boolValue: bool, // let mutable
|
boolValue: bool, // let mutable
|
||||||
@@ -372,7 +377,7 @@ module Ast {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func Ast_MakeExpr(kind: int, line: uint32, col: uint32) -> Expr {
|
func Ast_MakeExpr(kind: int, line: uint32, col: uint32) -> Expr {
|
||||||
return Expr { kind: kind, line: line, column: col,
|
return Expr { kind: kind, line: line, column: col, sourceFile: "",
|
||||||
strValue: "", intValue: 0, boolValue: false,
|
strValue: "", intValue: 0, boolValue: false,
|
||||||
tokKind: 0, tokText: "",
|
tokKind: 0, tokText: "",
|
||||||
child1: null as *Expr, child2: null as *Expr, child3: null as *Expr,
|
child1: null as *Expr, child2: null as *Expr, child3: null as *Expr,
|
||||||
@@ -410,7 +415,7 @@ module Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Ast_MakeStmt(kind: int, line: uint32, col: uint32) -> Stmt {
|
func Ast_MakeStmt(kind: int, line: uint32, col: uint32) -> Stmt {
|
||||||
return Stmt { kind: kind, line: line, column: col,
|
return Stmt { kind: kind, line: line, column: col, sourceFile: "",
|
||||||
strValue: "", boolValue: false,
|
strValue: "", boolValue: false,
|
||||||
child1: null as *Expr, child2: null as *Expr, child3: null as *Expr,
|
child1: null as *Expr, child2: null as *Expr, child3: null as *Expr,
|
||||||
refStmtType: null as *TypeExpr, refStmtPattern: null as *Pattern,
|
refStmtType: null as *TypeExpr, refStmtPattern: null as *Pattern,
|
||||||
@@ -418,6 +423,101 @@ module Ast {
|
|||||||
elseIfCount: 0 };
|
elseIfCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stamp sourceFile on an expression tree (empty slots only — keeps grafted macro nodes).
|
||||||
|
func Ast_StampExprFile(e: *Expr, path: String) {
|
||||||
|
if e == null as *Expr { return; }
|
||||||
|
if path == null as String || String_Eq(path, "") { return; }
|
||||||
|
if e.sourceFile == null as String || String_Eq(e.sourceFile, "") {
|
||||||
|
e.sourceFile = path;
|
||||||
|
}
|
||||||
|
Ast_StampExprFile(e.child1, path);
|
||||||
|
Ast_StampExprFile(e.child2, path);
|
||||||
|
Ast_StampExprFile(e.child3, path);
|
||||||
|
if e.refBlock != null as *Block {
|
||||||
|
Ast_StampBlockFile(e.refBlock, path);
|
||||||
|
}
|
||||||
|
var args: *ExprList = e.callArgs;
|
||||||
|
while args != null as *ExprList {
|
||||||
|
Ast_StampExprFile(args.expr, path);
|
||||||
|
args = args.next;
|
||||||
|
}
|
||||||
|
// Match arms (guard lives on Pattern as patGuardExpr when pkGuarded)
|
||||||
|
var arm: *MatchArm = e.matchArms;
|
||||||
|
while arm != null as *MatchArm {
|
||||||
|
Ast_StampExprFile(arm.body, path);
|
||||||
|
if arm.pattern != null as *Pattern {
|
||||||
|
Ast_StampPatternFile(arm.pattern, path);
|
||||||
|
}
|
||||||
|
arm = arm.next;
|
||||||
|
}
|
||||||
|
// Closure default param exprs
|
||||||
|
if e.closureParams != null as *Decl {
|
||||||
|
var pi: int = 0;
|
||||||
|
while pi < e.closureParams.paramCount {
|
||||||
|
var p: *Param = null as *Param;
|
||||||
|
if pi == 0 { p = &e.closureParams.param0; }
|
||||||
|
else if pi == 1 { p = &e.closureParams.param1; }
|
||||||
|
else if pi == 2 { p = &e.closureParams.param2; }
|
||||||
|
else if pi == 3 { p = &e.closureParams.param3; }
|
||||||
|
else if pi == 4 { p = &e.closureParams.param4; }
|
||||||
|
else if pi == 5 { p = &e.closureParams.param5; }
|
||||||
|
else if pi == 6 { p = &e.closureParams.param6; }
|
||||||
|
else if pi == 7 { p = &e.closureParams.param7; }
|
||||||
|
else if pi == 8 { p = &e.closureParams.param8; }
|
||||||
|
if p != null as *Param && p.defaultExpr != null as *Expr {
|
||||||
|
Ast_StampExprFile(p.defaultExpr, path);
|
||||||
|
}
|
||||||
|
pi = pi + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ast_StampPatternFile(pat: *Pattern, path: String) {
|
||||||
|
if pat == null as *Pattern { return; }
|
||||||
|
if path == null as String || String_Eq(path, "") { return; }
|
||||||
|
if pat.patGuardExpr != null as *Expr {
|
||||||
|
Ast_StampExprFile(pat.patGuardExpr, path);
|
||||||
|
}
|
||||||
|
Ast_StampPatternFile(pat.patChild1, path);
|
||||||
|
Ast_StampPatternFile(pat.patChild2, path);
|
||||||
|
Ast_StampPatternFile(pat.patArgs, path);
|
||||||
|
Ast_StampPatternFile(pat.patNext, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ast_StampBlockFile(b: *Block, path: String) {
|
||||||
|
if b == null as *Block { return; }
|
||||||
|
if path == null as String || String_Eq(path, "") { return; }
|
||||||
|
if b.sourceFile == null as String || String_Eq(b.sourceFile, "") {
|
||||||
|
b.sourceFile = path;
|
||||||
|
}
|
||||||
|
var s: *Stmt = b.firstStmt;
|
||||||
|
while s != null as *Stmt {
|
||||||
|
Ast_StampStmtFile(s, path);
|
||||||
|
s = s.nextStmt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ast_StampStmtFile(s: *Stmt, path: String) {
|
||||||
|
if s == null as *Stmt { return; }
|
||||||
|
if path == null as String || String_Eq(path, "") { return; }
|
||||||
|
if s.sourceFile == null as String || String_Eq(s.sourceFile, "") {
|
||||||
|
s.sourceFile = path;
|
||||||
|
}
|
||||||
|
Ast_StampExprFile(s.child1, path);
|
||||||
|
Ast_StampExprFile(s.child2, path);
|
||||||
|
Ast_StampExprFile(s.child3, path);
|
||||||
|
if s.refStmtBlock != null as *Block {
|
||||||
|
Ast_StampBlockFile(s.refStmtBlock, path);
|
||||||
|
}
|
||||||
|
if s.refStmtElse != null as *Block {
|
||||||
|
Ast_StampBlockFile(s.refStmtElse, path);
|
||||||
|
}
|
||||||
|
if s.refStmtDecl != null as *Decl {
|
||||||
|
// nested decl inside stmt — body stamp via Decl stamp
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Ast_MakeDecl(kind: int, line: uint32, col: uint32) -> Decl {
|
func Ast_MakeDecl(kind: int, line: uint32, col: uint32) -> Decl {
|
||||||
return Decl { kind: kind, line: line, column: col, isPublic: false,
|
return Decl { kind: kind, line: line, column: col, isPublic: false,
|
||||||
sourceFile: "",
|
sourceFile: "",
|
||||||
|
|||||||
+28
-1
@@ -611,10 +611,19 @@ func Cli_CollectStdlibImports(mod: *Module, outPaths: *String, maxCount: int, st
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stamp sourceFile on a decl and nested module items (for multi-file #line).
|
/// Stamp sourceFile on a decl, nested items, and body Expr/Stmt/Block trees.
|
||||||
|
/// Prepares multi-file #line and future macro / cross-file grafts (empty slots only).
|
||||||
func Cli_StampSourceFile(decl: *Decl, path: String) {
|
func Cli_StampSourceFile(decl: *Decl, path: String) {
|
||||||
if decl == null as *Decl { return; }
|
if decl == null as *Decl { return; }
|
||||||
decl.sourceFile = path;
|
decl.sourceFile = path;
|
||||||
|
// Function / method body
|
||||||
|
if decl.refBody != null as *Block {
|
||||||
|
Ast_StampBlockFile(decl.refBody, path);
|
||||||
|
}
|
||||||
|
// Const / type alias init exprs if present
|
||||||
|
if decl.constValue != null as *Expr {
|
||||||
|
Ast_StampExprFile(decl.constValue, path);
|
||||||
|
}
|
||||||
if decl.kind == dkModule {
|
if decl.kind == dkModule {
|
||||||
var inner: *Decl = decl.childDecl1;
|
var inner: *Decl = decl.childDecl1;
|
||||||
while inner != null as *Decl {
|
while inner != null as *Decl {
|
||||||
@@ -630,6 +639,24 @@ func Cli_StampSourceFile(decl: *Decl, path: String) {
|
|||||||
m = m.childDecl2;
|
m = m.childDecl2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Default param values
|
||||||
|
var pi: int = 0;
|
||||||
|
while pi < decl.paramCount {
|
||||||
|
var p: *Param = null as *Param;
|
||||||
|
if pi == 0 { p = &decl.param0; }
|
||||||
|
else if pi == 1 { p = &decl.param1; }
|
||||||
|
else if pi == 2 { p = &decl.param2; }
|
||||||
|
else if pi == 3 { p = &decl.param3; }
|
||||||
|
else if pi == 4 { p = &decl.param4; }
|
||||||
|
else if pi == 5 { p = &decl.param5; }
|
||||||
|
else if pi == 6 { p = &decl.param6; }
|
||||||
|
else if pi == 7 { p = &decl.param7; }
|
||||||
|
else if pi == 8 { p = &decl.param8; }
|
||||||
|
if p != null as *Param && p.defaultExpr != null as *Expr {
|
||||||
|
Ast_StampExprFile(p.defaultExpr, path);
|
||||||
|
}
|
||||||
|
pi = pi + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Cli_MergeFileInto(target: *Module, path: String, skipNames: *String, skipCount: int) -> int {
|
func Cli_MergeFileInto(target: *Module, path: String, skipNames: *String, skipCount: int) -> int {
|
||||||
|
|||||||
+27
-10
@@ -81,6 +81,28 @@ module HirLower {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefer Expr/Stmt.sourceFile (macro / cross-file graft); else enclosing Decl path.
|
||||||
|
func Lcx_SourceFileFor(ctx: *LowerCtx, nodeFile: String) -> String {
|
||||||
|
if nodeFile != null as String && !String_Eq(nodeFile, "") {
|
||||||
|
return nodeFile;
|
||||||
|
}
|
||||||
|
if ctx.currentSourceFile != null as String && !String_Eq(ctx.currentSourceFile, "") {
|
||||||
|
return ctx.currentSourceFile;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set line/column/sourceFile on a freshly allocated HirNode from AST location.
|
||||||
|
func Lcx_SetNodeLoc(ctx: *LowerCtx, n: *HirNode, line: uint32, col: uint32, nodeFile: String) {
|
||||||
|
if n == null as *HirNode { return; }
|
||||||
|
n.line = line;
|
||||||
|
n.column = col;
|
||||||
|
let f: String = Lcx_SourceFileFor(ctx, nodeFile);
|
||||||
|
if f != null as String && !String_Eq(f, "") {
|
||||||
|
n.sourceFile = f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String {
|
func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String {
|
||||||
// Most recent rename wins (scan from end)
|
// Most recent rename wins (scan from end)
|
||||||
var i: int = ctx.patMapCount - 1;
|
var i: int = ctx.patMapCount - 1;
|
||||||
@@ -1000,15 +1022,13 @@ module HirLower {
|
|||||||
// Alloca result + found flag
|
// Alloca result + found flag
|
||||||
let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
allocaNode.kind = hAlloca;
|
allocaNode.kind = hAlloca;
|
||||||
allocaNode.line = line;
|
Lcx_SetNodeLoc(ctx, allocaNode, line, col, expr.sourceFile);
|
||||||
allocaNode.column = col;
|
|
||||||
allocaNode.strValue = resultName;
|
allocaNode.strValue = resultName;
|
||||||
allocaNode.typeName = typeName;
|
allocaNode.typeName = typeName;
|
||||||
|
|
||||||
let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
foundAlloca.kind = hAlloca;
|
foundAlloca.kind = hAlloca;
|
||||||
foundAlloca.line = line;
|
Lcx_SetNodeLoc(ctx, foundAlloca, line, col, expr.sourceFile);
|
||||||
foundAlloca.column = col;
|
|
||||||
foundAlloca.strValue = foundName;
|
foundAlloca.strValue = foundName;
|
||||||
foundAlloca.typeName = "bool";
|
foundAlloca.typeName = "bool";
|
||||||
allocaNode.child3 = foundAlloca;
|
allocaNode.child3 = foundAlloca;
|
||||||
@@ -1175,8 +1195,7 @@ module HirLower {
|
|||||||
let col: uint32 = expr.column;
|
let col: uint32 = expr.column;
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
n.kind = hBlock;
|
n.kind = hBlock;
|
||||||
n.line = line;
|
Lcx_SetNodeLoc(ctx, n, line, col, expr.sourceFile);
|
||||||
n.column = col;
|
|
||||||
|
|
||||||
let kind: int = expr.kind;
|
let kind: int = expr.kind;
|
||||||
|
|
||||||
@@ -2387,8 +2406,7 @@ module HirLower {
|
|||||||
let col: uint32 = stmt.column;
|
let col: uint32 = stmt.column;
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
n.kind = hBlock;
|
n.kind = hBlock;
|
||||||
n.line = line;
|
Lcx_SetNodeLoc(ctx, n, line, col, stmt.sourceFile);
|
||||||
n.column = col;
|
|
||||||
|
|
||||||
let kind: int = stmt.kind;
|
let kind: int = stmt.kind;
|
||||||
|
|
||||||
@@ -3397,8 +3415,7 @@ module HirLower {
|
|||||||
// Wrap in an hBlock node with child1 = first statement in chain
|
// Wrap in an hBlock node with child1 = first statement in chain
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||||
n.kind = hBlock;
|
n.kind = hBlock;
|
||||||
n.line = block.line;
|
Lcx_SetNodeLoc(ctx, n, block.line, block.column, block.sourceFile);
|
||||||
n.column = block.column;
|
|
||||||
n.boolValue = true;
|
n.boolValue = true;
|
||||||
n.child1 = firstNode;
|
n.child1 = firstNode;
|
||||||
if asExpr && !String_Eq(yieldName, "") {
|
if asExpr && !String_Eq(yieldName, "") {
|
||||||
|
|||||||
+29
-1
@@ -138,4 +138,32 @@ if echo "$util_body" | grep -qE '#line [0-9]+ ".*Main\.bux"'; then
|
|||||||
fi
|
fi
|
||||||
echo " HirNode sourceFile: PASS (Util_Double stmts → Util.bux)"
|
echo " HirNode sourceFile: PASS (Util_Double stmts → Util.bux)"
|
||||||
|
|
||||||
echo "PASS: selfhost smoke (move_field + multi-file #line + HirNode sourceFile)"
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4) Expr/Stmt sourceFile — statement maps still per-file after AST stamp
|
||||||
|
# (Util body must not pick up Main.bux via empty inherit)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "=== selfhost: Expr/Stmt sourceFile stamp ==="
|
||||||
|
# Re-use mfline main.c: every #line inside Util_Double already checked;
|
||||||
|
# additionally ensure Main body has no Util.bux (symmetric isolation).
|
||||||
|
main_body=$(awk '
|
||||||
|
/#line 1 ".*Main\.bux"/ { grab=1 }
|
||||||
|
grab { print }
|
||||||
|
grab && /^}/ { exit }
|
||||||
|
' "$MAIN_C")
|
||||||
|
if [[ -z "$main_body" ]]; then
|
||||||
|
echo "error: could not find Main definition block" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if echo "$main_body" | grep -qE '#line [0-9]+ ".*Util\.bux"'; then
|
||||||
|
echo "error: Main body has #line Util.bux (Expr/Stmt stamp leak)" >&2
|
||||||
|
echo "$main_body" | grep -E '#line '
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! echo "$main_body" | grep -vE '#line 1 "' | grep -qE '#line [0-9]+ ".*Main\.bux"'; then
|
||||||
|
echo "error: Main body missing statement #line Main.bux" >&2
|
||||||
|
echo "$main_body" | head -30
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " Expr/Stmt sourceFile: PASS (Main stmts → Main.bux only)"
|
||||||
|
|
||||||
|
echo "PASS: selfhost smoke (move_field + multi-file #line + HirNode/Expr sourceFile)"
|
||||||
|
|||||||
Reference in New Issue
Block a user