diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 266dd11..912ec78 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 — **LSP 0.14 workspace imports**, implementation, selfhost-loop CI +> **Текущо:** v0.5.x — Expr/Stmt sourceFile, LSP 0.14, selfhost-loop CI > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден 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 -2. Fix selfhost C backend so buxc2→buxc3 fixed-point is green -3. Main PR CI workflow (`make test`) beyond optional selfhost-loop -4. LSP type hierarchy / prepareTypeHierarchy (optional) +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 diff --git a/src/ast.bux b/src/ast.bux index f0d88f0..83b58d7 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -138,6 +138,8 @@ module Ast { kind: int, line: uint32, column: uint32, + // Source file for multi-file #line / future macros (empty = inherit Decl) + sourceFile: String, // Common fields strValue: String, // ident name, path segments, field name, callee intValue: int, // operator kind, intrinsic kind @@ -193,6 +195,7 @@ module Ast { struct Block { line: uint32, column: uint32, + sourceFile: String, // multi-file #line (empty = inherit Decl) stmtCount: int, firstStmt: *Stmt, lastStmt: *Stmt, @@ -227,6 +230,8 @@ module Ast { kind: int, line: uint32, column: uint32, + // Source file for multi-file #line / future macros (empty = inherit Decl) + sourceFile: String, // Common fields strValue: String, // let name, pattern ident, label, for var boolValue: bool, // let mutable @@ -372,7 +377,7 @@ module Ast { // --------------------------------------------------------------------------- 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, tokKind: 0, tokText: "", 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 { - return Stmt { kind: kind, line: line, column: col, + return Stmt { kind: kind, line: line, column: col, sourceFile: "", strValue: "", boolValue: false, child1: null as *Expr, child2: null as *Expr, child3: null as *Expr, refStmtType: null as *TypeExpr, refStmtPattern: null as *Pattern, @@ -418,6 +423,101 @@ module Ast { 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 { return Decl { kind: kind, line: line, column: col, isPublic: false, sourceFile: "", diff --git a/src/cli.bux b/src/cli.bux index baca902..5dc2918 100644 --- a/src/cli.bux +++ b/src/cli.bux @@ -611,10 +611,19 @@ func Cli_CollectStdlibImports(mod: *Module, outPaths: *String, maxCount: int, st 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) { if decl == null as *Decl { return; } 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 { var inner: *Decl = decl.childDecl1; while inner != null as *Decl { @@ -630,6 +639,24 @@ func Cli_StampSourceFile(decl: *Decl, path: String) { 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 { diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 77296cb..714124d 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -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 { // Most recent rename wins (scan from end) var i: int = ctx.patMapCount - 1; @@ -1000,15 +1022,13 @@ module HirLower { // Alloca result + found flag let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; allocaNode.kind = hAlloca; - allocaNode.line = line; - allocaNode.column = col; + Lcx_SetNodeLoc(ctx, allocaNode, line, col, expr.sourceFile); allocaNode.strValue = resultName; allocaNode.typeName = typeName; let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; foundAlloca.kind = hAlloca; - foundAlloca.line = line; - foundAlloca.column = col; + Lcx_SetNodeLoc(ctx, foundAlloca, line, col, expr.sourceFile); foundAlloca.strValue = foundName; foundAlloca.typeName = "bool"; allocaNode.child3 = foundAlloca; @@ -1175,8 +1195,7 @@ module HirLower { let col: uint32 = expr.column; let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; n.kind = hBlock; - n.line = line; - n.column = col; + Lcx_SetNodeLoc(ctx, n, line, col, expr.sourceFile); let kind: int = expr.kind; @@ -2387,8 +2406,7 @@ module HirLower { let col: uint32 = stmt.column; let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; n.kind = hBlock; - n.line = line; - n.column = col; + Lcx_SetNodeLoc(ctx, n, line, col, stmt.sourceFile); let kind: int = stmt.kind; @@ -3397,8 +3415,7 @@ module HirLower { // Wrap in an hBlock node with child1 = first statement in chain let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; n.kind = hBlock; - n.line = block.line; - n.column = block.column; + Lcx_SetNodeLoc(ctx, n, block.line, block.column, block.sourceFile); n.boolValue = true; n.child1 = firstNode; if asExpr && !String_Eq(yieldName, "") { diff --git a/tools/smoke_selfhost.sh b/tools/smoke_selfhost.sh index 10ea32d..09c82df 100755 --- a/tools/smoke_selfhost.sh +++ b/tools/smoke_selfhost.sh @@ -138,4 +138,32 @@ if echo "$util_body" | grep -qE '#line [0-9]+ ".*Main\.bux"'; then fi 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)"