From 340be402a56759bd966bf44373cd92efeeedab5b Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 11 Jun 2026 19:24:46 +0300 Subject: [PATCH] ir: whole-program passes pin the source context per decl (fix 0122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit convergeClosureShapeSets, checkErrorFlow, and the unknown-type loop ran under whatever current_source_file the previous phase left behind — closure-literal annotations resolved (and reject/unknown-type diagnostics rendered) against an arbitrary module. Latent while std.sx was a single file (the ambient happened to be the main file); the re-export facade restructure exposed it. Each walk now pins setCurrentSourceFile per decl / per fn (body.source_file is already stamped by resolveImports). Coverage: examples 0129/1047/1049/1052/ 1053/1056 against the facade std.sx. Gates: zbt 426/426, suite 588/588. --- ...e-program-passes-ambient-source-context.md | 64 +++++++++++++++++++ src/ir/error_analysis.zig | 8 +++ src/ir/error_flow.zig | 7 ++ src/ir/semantic_diagnostics.zig | 5 ++ 4 files changed, 84 insertions(+) create mode 100644 issues/0122-whole-program-passes-ambient-source-context.md diff --git a/issues/0122-whole-program-passes-ambient-source-context.md b/issues/0122-whole-program-passes-ambient-source-context.md new file mode 100644 index 0000000..bd55554 --- /dev/null +++ b/issues/0122-whole-program-passes-ambient-source-context.md @@ -0,0 +1,64 @@ +# 0122 — whole-program passes resolve/diagnose under a stale ambient source + +> **RESOLVED** (2026-06-11, same session — found and fixed during the +> std.sx-as-pure-re-exports restructure, Agra-directed). Three +> whole-program passes ran under whatever `current_source_file` the +> previous pipeline phase happened to leave behind, instead of pinning +> the context per declaration: +> +> 1. `ErrorAnalysis.convergeClosureShapeSets` (error_analysis.zig) — +> resolves closure-literal param/return annotations; a stale context +> made example-declared nominal types (`Point`, `Color`) fail the E4 +> visibility gate with `type 'X' is not visible` attributed to +> nonsense std.sx spans. Fixed: pin `setCurrentSourceFile` per +> `fn_ast_map` entry from `body.source_file` (already stamped by +> resolveImports). +> 2. `ErrorFlow.checkErrorFlow` (error_flow.zig) — the flow walk +> resolves types via `inferExprType` AND emits its reject +> diagnostics; both used the ambient file. Fixed: pin per decl. +> 3. The `UnknownTypeChecker` unknown-type loop +> (semantic_diagnostics.zig) — emitted with the ambient file +> (`checkBindingNames` beside it already saved/restored per node). +> Fixed: pin `diagnostics.current_source_file` per decl. +> +> Latent on master for all three — the ambient just happened to be the +> main file with the old single-file std.sx; the restructured std.sx +> (namespace part-file imports) reordered the pipeline's last-touched +> module and exposed them. Pinned coverage: examples 0129 / 1047 / +> 1049 / 1052 / 1053 / 1056 (closure shapes with nominal types, +> error-flow reject attribution) fail without the fixes once std.sx is +> the re-export facade. Gates: zig build test 426/426, suite 588/588. + +## Symptom + +With a std.sx whose first declarations are namespace imports, programs +using closures with user-struct parameter types failed +`type 'Point' is not visible; #import the module that declares it` +attributed to meaningless std.sx spans, and error-flow / unknown-type +diagnostics for main-file code rendered against std.sx's line table +(e.g. expected `examples/foo.sx:22:21`, got `std.sx:16:25`). + +## Reproduction + +Against the pre-fix compiler with the re-export std.sx: + +```sx +#import "modules/std.sx"; +Point :: struct { x, y: s32; } +main :: () { + f := closure((p: Point) -> Point => Point.{ x = p.x + 1, y = p.y }); + r := f(Point.{ x = 1, y = 2 }); + out("done\n"); +} +``` + +## Investigation prompt + +(Resolved — kept for the record.) The root pattern: any pass that runs +after module scanning and either resolves source-gated names or emits +diagnostics MUST pin the visibility/rendering context per declaration +(`setCurrentSourceFile(decl.source_file)` — syncs the lowering context +and the diagnostics renderer), never inherit the ambient. Fn bodies +carry `body.source_file` (stamped by resolveImports) for fn-keyed +walks. When auditing for siblings, check every `lowerRoot` phase that +walks `fn_ast_map` or the program decl list. diff --git a/src/ir/error_analysis.zig b/src/ir/error_analysis.zig index f9f9e94..92a45e9 100644 --- a/src/ir/error_analysis.zig +++ b/src/ir/error_analysis.zig @@ -191,8 +191,16 @@ pub const ErrorAnalysis = struct { /// by all occurrences of its value-signature shape. A `try slot(x)` against /// any matching-shape slot then widens against this union. pub fn convergeClosureShapeSets(self: ErrorAnalysis) void { + // Pin the visibility context to each fn's DEFINING module + // (body.source_file, stamped by resolveImports) — a closure literal's + // param/return annotations must resolve where the fn is written, not + // against whatever module the previous pipeline phase happened to + // leave as the ambient context (issue 0122). + const saved = self.l.current_source_file; + defer self.l.setCurrentSourceFile(saved); var it = self.l.program_index.fn_ast_map.iterator(); while (it.next()) |e| { + self.l.setCurrentSourceFile(e.value_ptr.*.body.source_file orelse saved); self.collectClosureShapes(e.value_ptr.*.body); } } diff --git a/src/ir/error_flow.zig b/src/ir/error_flow.zig index 7ea1c20..b3d7df1 100644 --- a/src/ir/error_flow.zig +++ b/src/ir/error_flow.zig @@ -71,12 +71,19 @@ pub const ErrorFlow = struct { /// this conservative check would over-reject), so they are skipped. pub fn checkErrorFlow(self: ErrorFlow, decls: []const *const Node) void { if (self.l.diagnostics == null) return; + const saved_file = self.l.current_source_file; + defer self.l.setCurrentSourceFile(saved_file); for (decls) |decl| { if (self.l.main_file) |mf| { if (decl.source_file) |sf| { if (!std.mem.eql(u8, sf, mf)) continue; } } + // Pin the visibility context (and diagnostic rendering) to the + // decl's own module — the flow walk resolves types via + // inferExprType, and the ambient file the previous phase left + // behind is arbitrary (issue 0122). + if (decl.source_file) |sf| self.l.setCurrentSourceFile(sf); switch (decl.data) { .fn_decl => |fd| self.analyzeFnBody(fd.body), .const_decl => |cd| { diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 3ca9851..8393c20 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -65,12 +65,17 @@ pub const UnknownTypeChecker = struct { var declared = std.StringHashMap(void).init(self.alloc); defer declared.deinit(); self.collectDeclaredTypeNames(decls, &declared); + const saved_file = self.diagnostics.current_source_file; + defer self.diagnostics.current_source_file = saved_file; for (decls) |decl| { if (self.main_file) |mf| { if (decl.source_file) |sf| { if (!std.mem.eql(u8, sf, mf)) continue; } } + // Render against the decl's own module, not the ambient file the + // previous phase left behind (issue 0122). + if (decl.source_file) |sf| self.diagnostics.current_source_file = sf; switch (decl.data) { .fn_decl => self.checkFnSignatureTypes(&decl.data.fn_decl, &declared), .struct_decl => |sd| self.checkStructFieldTypes(&sd, &declared),