refactor(stdlib/S1.2): delete module_fns; dissolve legacy_direct_any [additive]

Delete module_fns as a separate function-author fact source. Its authors
already live in the module_decls raw facts, so lowerRetainedSameNameAuthors now
reads function authors straight out of module_decls (filtered to *FnDecl via
fnDeclOfRaw) — the same path → name → RawDeclRef store, fn-filtered. Remove
imports.ModuleFns / FnIndex / indexModuleFns / buildModuleFns / fnDeclOf, the
Compilation.module_fns field + its build + wiring, and ProgramIndex.module_fns.

Remove VisibilityMode.legacy_direct_any (the quarantined own-scope-plus-full-
import_graph mode): no production caller passed it, so the collectVisibleAuthors
and isVisible switch arms that handled it are dead and go too, collapsing
VisEdgeSet to the single flat-import walk. No semantic fallback is introduced;
import_graph stays the transitive-visibility source for findVisibleImpls.

Additive: the old maps stay active and lowering still consumes them — no
lowering consumer is cut over to the DeclTable (that is S3), and no resolution
behavior changes. Tests that drove the removed symbols are rerouted through
module_decls / the flat-edge walk.

Gate over the baseline-green corpus: zig build, zig build test (424/424),
bash tests/run_examples.sh (540 passed) — all exit 0; single-author output
byte-identical; multi-author 0722–0740 stdout/exit unchanged.
This commit is contained in:
agra
2026-06-09 11:36:04 +03:00
parent 8058be2538
commit cd7510067f
8 changed files with 98 additions and 180 deletions

View File

@@ -29,9 +29,6 @@ pub const Compilation = struct {
/// Flat-only subset of `import_graph` (bare `#import` edges, no namespaced
/// `ns :: #import`). Borrowed by `ProgramIndex.flat_import_graph`.
flat_import_graph: std.StringHashMap(std.StringHashMap(void)),
/// Per-module authored-function index (`path → name → *const FnDecl`).
/// Borrowed by `ProgramIndex.module_fns`.
module_fns: imports.ModuleFns,
/// Per-module scalar raw-decl index (`path → name → RawDeclRef`), built by
/// `imports.buildImportFacts`. Borrowed by `ProgramIndex.module_decls`.
module_decls: imports.ModuleDecls,
@@ -69,7 +66,6 @@ pub const Compilation = struct {
.module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator),
.import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator),
.flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator),
.module_fns = imports.ModuleFns.init(allocator),
.module_decls = imports.ModuleDecls.init(allocator),
.namespace_edges = imports.NamespaceEdges.init(allocator),
.decl_table = imports.DeclTable.init(allocator),
@@ -133,11 +129,6 @@ pub const Compilation = struct {
self.module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope) catch {};
}
// Per-module authored-function index, built from the SAME modules as
// `module_scopes` (main + every cache entry). Keyed by path; same-name
// cross-module authors stay distinct under their own paths.
imports.buildModuleFns(self.allocator, self.file_path, mod, &cache, &self.module_fns) catch {};
// Raw import facts (the unified-resolver store): scalar per-module
// raw-decl index + namespace edges, built from the SAME modules. Nothing
// consumes these yet — they are borrowed by `ProgramIndex` for later
@@ -320,7 +311,6 @@ pub const Compilation = struct {
lowering.program_index.module_scopes = &self.module_scopes;
lowering.program_index.import_graph = &self.import_graph;
lowering.program_index.flat_import_graph = &self.flat_import_graph;
lowering.program_index.module_fns = &self.module_fns;
lowering.program_index.module_decls = &self.module_decls;
lowering.program_index.namespace_edges = &self.namespace_edges;
lowering.program_index.decl_table = &self.decl_table;

View File

@@ -70,11 +70,11 @@ fn hasErr(diags: *const errors.DiagnosticList, needle: []const u8) bool {
}
// Two flat-imported modules each author `greet`; a third is namespaced. The
// step retains BOTH `greet` authors under their own paths in `module_fns` and
// records the namespaced import in `import_graph` but NOT in `flat_import_graph`
// — WITHOUT touching the merged scope: `mod.decls` stays byte-for-byte
// first-wins (one `greet`, a.sx's), exactly as on `wt-fix-0102-base`.
test "imports: module_fns retains same-name cross-module fns; flat_import_graph excludes namespaced edge" {
// raw facts retain BOTH `greet` authors under their own paths in `module_decls`
// (function authors flow through it) and record the namespaced import in
// `import_graph` but NOT in `flat_import_graph` — WITHOUT touching the merged
// scope: `mod.decls` stays byte-for-byte first-wins (one `greet`, a.sx's).
test "imports: module_decls retains same-name cross-module fns; flat_import_graph excludes namespaced edge" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
@@ -131,8 +131,7 @@ test "imports: module_fns retains same-name cross-module fns; flat_import_graph
.{},
);
var module_fns = imports.ModuleFns.init(alloc);
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
// The MERGED scope the first-wins resolver consumes is unchanged: mergeFlat
// still drops the second `greet`, so `mod.decls` carries exactly ONE — and
@@ -147,12 +146,18 @@ test "imports: module_fns retains same-name cross-module fns; flat_import_graph
}
try std.testing.expectEqual(@as(usize, 1), greet_count);
// module_fns retains BOTH authors of `greet`, keyed by their own paths —
// module_decls retains BOTH authors of `greet`, keyed by their own paths —
// the dropped author is recorded here (side index), not in the merged scope.
const a_fns = module_fns.get(a_path) orelse return error.MissingAFns;
const b_fns = module_fns.get(b_path) orelse return error.MissingBFns;
const a_greet = a_fns.get("greet") orelse return error.MissingAGreet;
const b_greet = b_fns.get("greet") orelse return error.MissingBGreet;
const a_idx = facts.decls.get(a_path) orelse return error.MissingAIndex;
const b_idx = facts.decls.get(b_path) orelse return error.MissingBIndex;
const a_greet = switch (a_idx.names.get("greet") orelse return error.MissingAGreet) {
.fn_decl => |fd| fd,
else => return error.AGreetNotFn,
};
const b_greet = switch (b_idx.names.get("greet") orelse return error.MissingBGreet) {
.fn_decl => |fd| fd,
else => return error.BGreetNotFn,
};
// Distinct authoring decls — not the same node deduped down to one.
try std.testing.expect(a_greet != b_greet);
// First-wins: the surviving merged-scope `greet` is a.sx's author.
@@ -175,9 +180,9 @@ test "imports: module_fns retains same-name cross-module fns; flat_import_graph
// Mixed collision: a.sx authors `Widget` as a STRUCT (non-fn), b.sx authors it
// as a FUNCTION. fix-0102a must NOT let the function-author retention shift the
// merged scope — first-wins keeps a.sx's struct and drops b.sx's function,
// exactly as on `wt-fix-0102-base`. (The fn author may still be indexed in
// module_fns; resolution is what must be untouched.)
// merged scope — first-wins keeps a.sx's struct and drops b.sx's function.
// (The fn author may still be indexed in `module_decls`; resolution is what
// must be untouched.)
test "imports: mixed non-fn/fn same-name collision stays first-wins in merged scope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();

View File

@@ -5,18 +5,6 @@ const errors = @import("errors.zig");
const c_import = @import("c_import.zig");
const Node = ast.Node;
/// The `*const ast.FnDecl` a function-authoring decl carries, or null when the
/// decl is not a function — either a bare `fn_decl` (`f :: (…) -> T { … }`) or a
/// `const_decl` whose value is a function. Drives the per-module `module_fns`
/// identity index (fix-0102a).
fn fnDeclOf(decl: *const Node) ?*const ast.FnDecl {
return switch (decl.data) {
.fn_decl => &decl.data.fn_decl,
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
else => null,
};
}
/// Comptime evaluation context for the inline-if hoisting pass below.
/// Mirrors the values `injectComptimeConstants` will later push into the
/// lowering's `comptime_constants` map (OS / ARCH / POINTER_SIZE), but
@@ -451,50 +439,15 @@ pub const ResolvedModule = struct {
/// Module cache: maps resolved file paths to their ResolvedModules.
pub const ModuleCache = std.StringHashMap(ResolvedModule);
/// Per-module function identity index: function NAME → the `*const FnDecl` that
/// module AUTHORS. Mirrors a single module's slice of `module_scopes`.
pub const FnIndex = std.StringHashMap(*const ast.FnDecl);
/// `path → name → *const FnDecl`, mirroring `module_scopes`. One entry per
/// resolved module keyed by its path (a directory's combined module keyed by
/// `dir_path`); each entry indexes only what that module AUTHORS. Two modules
/// each authoring `f` are retained under their own paths — the identity index
/// fix-0102c's bare-name disambiguation consults to bind a flat call to the
/// right author.
pub const ModuleFns = std.StringHashMap(FnIndex);
/// Index a single module's authored functions (`own_decls`) into `out[path]`.
/// First-wins WITHIN a module mirrors the scan pass; cross-module same-name
/// authors live under their own `path` keys.
fn indexModuleFns(allocator: std.mem.Allocator, out: *ModuleFns, path: []const u8, own_decls: []const *Node) !void {
const gop = try out.getOrPut(path);
if (!gop.found_existing) gop.value_ptr.* = FnIndex.init(allocator);
for (own_decls) |decl| {
const fd = fnDeclOf(decl) orelse continue;
const name = decl.data.declName() orelse continue;
if (gop.value_ptr.contains(name)) continue;
try gop.value_ptr.put(name, fd);
}
}
/// Build the per-module function index from a resolved program: the main module
/// (keyed by `main_path`) plus every cached module (keyed by its own path).
/// Mirrors how `core.zig` fills `module_scopes` from `mod.scope` + the cache.
pub fn buildModuleFns(allocator: std.mem.Allocator, main_path: []const u8, main_mod: ResolvedModule, cache: *const ModuleCache, out: *ModuleFns) !void {
try indexModuleFns(allocator, out, main_path, main_mod.own_decls);
var it = cache.iterator();
while (it.next()) |entry| {
try indexModuleFns(allocator, out, entry.key_ptr.*, entry.value_ptr.own_decls);
}
}
// ── Raw import facts (the unified-resolver store; nothing consumes it yet) ──
// ── Raw import facts (the unified-resolver store) ──
//
// `buildImportFacts` produces two source-keyed views over the resolved program,
// callable WITHOUT IR lowering (the LSP reuses it later): a scalar per-module
// raw-decl index (`name → RawDeclRef`) and the namespace import edges
// (`importer → alias → NamespaceTarget`). Both are built from each module's
// `own_decls` — exactly the same modules `buildModuleFns` walks.
// `own_decls` (the main module plus every cache entry). Function authors are
// read out of `name → RawDeclRef` directly (`fnDeclOfRaw`), so there is no
// separate function-only index.
/// A named top-level declaration the resolver may select, kept as the raw AST
/// node pointer (NOT pre-classified — a `const_decl` whose value is a function
@@ -566,9 +519,9 @@ pub fn rawDeclRefOf(decl: *const Node) ?RawDeclRef {
}
/// Index one module's authored decls (`own_decls`) into `decls[path]` and record
/// any namespace aliases into `ns_edges[path]`. First-wins WITHIN a module
/// mirrors `indexModuleFns`; `own_decls` is already name-deduped by `addOwnDecl`,
/// so the first-wins guard never actually fires here.
/// any namespace aliases into `ns_edges[path]`. First-wins WITHIN a module;
/// `own_decls` is already name-deduped by `addOwnDecl`, so the first-wins guard
/// never actually fires here.
fn indexModuleDecls(
allocator: std.mem.Allocator,
decls: *ModuleDecls,
@@ -601,8 +554,8 @@ fn indexModuleDecls(
}
/// Build the raw import facts from a resolved program: the main module (keyed by
/// `main_path`) plus every cached module (keyed by its own path). The same module
/// set `buildModuleFns` walks. No IR lowering required.
/// `main_path`) plus every cached module (keyed by its own path). No IR lowering
/// required.
pub fn buildImportFacts(
allocator: std.mem.Allocator,
main_path: []const u8,

View File

@@ -1291,7 +1291,7 @@ fn countRealBodies(module: *ir_mod.Module, name: []const u8) usize {
// fix-0102b: two flat-imported modules each author `greet`. The first-wins merge
// keeps a.sx's author in the merged decl list (the WINNER) and drops b.sx's,
// which `module_fns` still retains (0102a). `main` itself can't bare-call `greet`
// which the `module_decls` raw facts still retain (0102a). `main` itself can't bare-call `greet`
// — under fix-0102c two flat authors make that ambiguous — so it calls a.sx's
// `use_greet` wrapper, whose own-author call to `greet` binds a.sx's winner.
// BEFORE the identity-addressable pass, only the winner has a real body — the
@@ -1358,11 +1358,9 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
while (cache_it.next()) |entry| {
try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope);
}
var module_fns = imports.ModuleFns.init(alloc);
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
// Phase A raw facts: `selectPlainCallableAuthor` (Phase C) collects authors
// over `module_decls`, not `module_fns`. Wired exactly as `core.zig` does.
// Phase A raw facts: both `selectPlainCallableAuthor` (Phase C) and
// `lowerRetainedSameNameAuthors` read function authors out of `module_decls`.
// Wired exactly as `core.zig` does.
var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
const resolved_root = try alloc.create(Node);
@@ -1378,7 +1376,6 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
lowering.program_index.module_scopes = &module_scopes;
lowering.program_index.import_graph = &import_graph;
lowering.program_index.flat_import_graph = &flat_import_graph;
lowering.program_index.module_fns = &module_fns;
lowering.program_index.module_decls = &facts.decls;
lowering.lowerRoot(resolved_root);
@@ -1409,23 +1406,23 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
// F1 (attempt-2): the identity map must be keyed by the STABLE AST field
// pointer for BOTH same-name authors — the exact pointers `fn_ast_map` and
// `module_fns` carry — not a per-iteration switch-capture temporary. If the
// winner were keyed by `&fd` (the scanDecls bug), this lookup by the stable
// `fn_ast_map` pointer would miss (null). fix-0102c routes calls through
// exactly these pointers, so the round-trip must hold here.
// the `module_decls` raw facts carry — not a per-iteration switch-capture
// temporary. If the winner were keyed by `&fd` (the scanDecls bug), this
// lookup by the stable `fn_ast_map` pointer would miss (null). fix-0102c
// routes calls through exactly these pointers, so the round-trip must hold.
const winner_fd = lowering.program_index.fn_ast_map.get("greet").?;
const winner_fid = lowering.fn_decl_fids.get(winner_fd);
try std.testing.expect(winner_fid != null);
// Round-trips to the first-wins winner FuncId (resolveFuncByName's pick).
try std.testing.expectEqual(lowering.resolveFuncByName("greet").?, winner_fid.?);
// The shadowed author's stable pointer lives in `module_fns`; find the one
// The shadowed author's stable pointer lives in `module_decls`; find the one
// that is NOT the winner and confirm IT round-trips to a DISTINCT FuncId.
var shadow_fd: ?*const ast.FnDecl = null;
var mf_it = module_fns.iterator();
while (mf_it.next()) |path_entry| {
if (path_entry.value_ptr.get("greet")) |fd| {
if (fd != winner_fd) shadow_fd = fd;
var md_it = facts.decls.iterator();
while (md_it.next()) |path_entry| {
if (path_entry.value_ptr.names.get("greet")) |ref| {
if (ref == .fn_decl and ref.fn_decl != winner_fd) shadow_fd = ref.fn_decl;
}
}
try std.testing.expect(shadow_fd != null);
@@ -1521,8 +1518,6 @@ test "lower: scan populates source-keyed caches per declaring source (E0)" {
while (cache_it.next()) |entry| {
try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope);
}
var module_fns = imports.ModuleFns.init(alloc);
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
const resolved_root = try alloc.create(Node);
@@ -1538,7 +1533,6 @@ test "lower: scan populates source-keyed caches per declaring source (E0)" {
lowering.program_index.module_scopes = &module_scopes;
lowering.program_index.import_graph = &import_graph;
lowering.program_index.flat_import_graph = &flat_import_graph;
lowering.program_index.module_fns = &module_fns;
lowering.program_index.module_decls = &facts.decls;
lowering.lowerRoot(resolved_root);

View File

@@ -986,10 +986,10 @@ pub const Lowering = struct {
// Declare extern stub for all functions (bodies lowered
// lazily). Key the identity map (`fn_decl_fids`, inside
// `declareFunction`) by the STABLE AST field pointer the
// same `&decl.data.fn_decl` stored in `fn_ast_map` and
// `module_fns` not the switch-capture copy `fd`, whose
// address is a per-iteration stack temporary that no later
// decl-identity lookup can reproduce.
// same `&decl.data.fn_decl` stored in `fn_ast_map` and the
// `module_decls` raw facts not the switch-capture copy `fd`,
// whose address is a per-iteration stack temporary that no
// later decl-identity lookup can reproduce.
self.declareFunction(&decl.data.fn_decl, fd.name);
},
.const_decl => |cd| {
@@ -1741,35 +1741,36 @@ pub const Lowering = struct {
/// The first-wins flat/directory merge keeps exactly one author per name in
/// the merged decl list; `scanDecls` declares that WINNER (lowered on demand
/// through the name-keyed `lazyLowerFunction`). fix-0102a retained every
/// dropped same-name author in `module_fns` (path name `*FnDecl`) without
/// touching resolution; this walks that index and gives each shadowed author
/// its own slot: `declareFunction` (identity-mapped to a fresh same-name
/// FuncId) + `lowerFunctionBodyInto` (its body, in its own module's
/// dropped same-name author in the `module_decls` raw facts (path name
/// `RawDeclRef`) without touching resolution; this walks that index, filters
/// each author to its `*FnDecl` (`fnDeclOfRaw`), and gives each shadowed
/// author its own slot: `declareFunction` (identity-mapped to a fresh
/// same-name FuncId) + `lowerFunctionBodyInto` (its body, in its own module's
/// visibility context). Two same-name authors then carry distinct FuncIds and
/// distinct bodies, while `resolveFuncByName` still returns the first (winner)
/// author so existing calls bind first-wins.
///
/// Scoped to DIRECT flat imports of the main file: a `module_fns` entry whose
/// path is the main file or one of its bare `#import` edges. A namespaced
/// (`ns :: #import`) author has no bare-name winner and is excluded both by
/// that flat-edge gate and by the `fn_ast_map` winner lookup below.
/// Scoped to DIRECT flat imports of the main file: a `module_decls` entry
/// whose path is the main file or one of its bare `#import` edges. A
/// namespaced (`ns :: #import`) author has no bare-name winner and is excluded
/// both by that flat-edge gate and by the `fn_ast_map` winner lookup below.
pub fn lowerRetainedSameNameAuthors(self: *Lowering) void {
const module_fns = self.program_index.module_fns orelse return;
const module_decls = self.program_index.module_decls orelse return;
const main_file = self.main_file orelse return;
const flat_graph = self.program_index.flat_import_graph orelse return;
const main_flat_edges = flat_graph.get(main_file);
var path_it = module_fns.iterator();
var path_it = module_decls.iterator();
while (path_it.next()) |path_entry| {
const path = path_entry.key_ptr.*;
const is_eligible = std.mem.eql(u8, path, main_file) or
(main_flat_edges != null and main_flat_edges.?.contains(path));
if (!is_eligible) continue;
var fn_it = path_entry.value_ptr.iterator();
var fn_it = path_entry.value_ptr.names.iterator();
while (fn_it.next()) |fn_entry| {
const name = fn_entry.key_ptr.*;
const fd = fn_entry.value_ptr.*;
const fd = fnDeclOfRaw(fn_entry.value_ptr.*) orelse continue;
// A name with no bare winner is namespaced-only (`ns.fn`) it
// never participated in the flat merge, so it has no shadow to
@@ -1871,18 +1872,18 @@ pub const Lowering = struct {
/// THE plain bare-name call selector (fix-0102c, R5 §C). `resolveBareCallee`'s
/// body verbatim, now over the Phase B author collector
/// (`resolver.collectVisibleAuthors` the ONE graph-walk) instead of a direct
/// `module_fns` + `flat_import_graph` traversal. Routes a bare identifier call
/// `name` from `caller_file` to the right same-name author when flat imports
/// introduce a genuine collision. Every single-author / local / parameter /
/// std / qualified name resolves through the EXISTING path unchanged: the
/// selector returns `.none` whenever the outcome would match first-wins, so
/// nothing on the common path is perturbed.
/// `module_decls` + `flat_import_graph` traversal. Routes a bare identifier
/// call `name` from `caller_file` to the right same-name author when flat
/// imports introduce a genuine collision. Every single-author / local /
/// parameter / std / qualified name resolves through the EXISTING path
/// unchanged: the selector returns `.none` whenever the outcome would match
/// first-wins, so nothing on the common path is perturbed.
///
/// The collector returns RAW authors across ALL decl domains; this selector
/// reproduces `module_fns`' fn-only view by filtering each author through
/// `fnDeclOfRaw` (a `const`-wrapped fn unwraps to its inner fn the exact
/// `*FnDecl` `module_fns` stored; every other domain drops out), preserving
/// resolveBareCallee's negative space byte-for-byte.
/// reproduces a fn-only author view by filtering each author through
/// `fnDeclOfRaw` (a `const`-wrapped fn unwraps to its inner fn; every other
/// domain drops out), preserving resolveBareCallee's negative space
/// byte-for-byte.
///
/// - **own-author wins**: if `caller_file` authors `name` as a fn and the
/// bare-name first-wins winner is a DIFFERENT author, select the caller's
@@ -1910,8 +1911,8 @@ pub const Lowering = struct {
// own-author wins. The collector's `own` spans all domains; a non-fn
// (or a const not bound to a function) means `caller_file` has no fn
// `name` fall through to the flat authors, exactly as the fn-only
// `module_fns` walk did.
// `name` fall through to the flat authors, exactly as a fn-only walk
// would.
if (set.own) |own_author| {
if (fnDeclOfRaw(own_author.raw)) |own| {
if (winner != null and winner.? == own) return .none;
@@ -2378,10 +2379,10 @@ pub const Lowering = struct {
}
/// The `*FnDecl` a raw author wraps, or null when the author is not a
/// function `imports.fnDeclOf` over a `RawDeclRef` so the collector's
/// all-domain authors reproduce `module_fns`' fn-only view (a `const`-wrapped
/// fn unwraps to its inner fn, the same pointer `module_fns` holds; every
/// other domain null).
/// function unwraps a `RawDeclRef` so the collector's all-domain authors
/// yield a fn-only view (a `const`-wrapped fn unwraps to its inner fn; every
/// other domain null). The single place function authors are read out of
/// the `module_decls` raw facts.
fn fnDeclOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.FnDecl {
return switch (ref) {
.fn_decl => |fd| fd,
@@ -2633,27 +2634,20 @@ pub const Lowering = struct {
const fd = self.program_index.fn_ast_map.get(name) orelse return true;
if (fd.body.data != .foreign_expr) return true;
if (fd.body.data.foreign_expr.library_ref != null) return true;
return self.visibleOverEdges(name, .flat);
return self.visibleOverEdges(name);
},
.user_bare_flat => return self.visibleOverEdges(name, .flat),
.legacy_direct_any => return self.visibleOverEdges(name, .all),
.user_bare_flat => return self.visibleOverEdges(name),
}
}
const VisEdgeSet = enum { flat, all };
/// Resolve the mode's edge set and run the per-file visibility walk. Falls
/// Run the per-file visibility walk over the flat-import edge set. Falls
/// open (visible) when the scoping infrastructure isn't wired (comptime
/// callers, directory imports without main_file, etc.). The caller is
/// responsible for restricting the check to names that ARE known top-level
/// decls; otherwise every local variable would be policed.
fn visibleOverEdges(self: *Lowering, name: []const u8, edges: VisEdgeSet) bool {
fn visibleOverEdges(self: *Lowering, name: []const u8) bool {
const source = self.current_source_file orelse return true;
const graph = switch (edges) {
.flat => self.program_index.flat_import_graph,
.all => self.program_index.import_graph,
};
return nameVisibleOverEdges(self.program_index.module_scopes, graph, source, name);
return nameVisibleOverEdges(self.program_index.module_scopes, self.program_index.flat_import_graph, source, name);
}
/// Check if a C-imported function is visible from the current source file.

View File

@@ -588,9 +588,9 @@ pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId };
/// `*ProgramIndex` instead of `*Lowering`.
///
/// OWNS the declaration maps below. BORROWS `module_scopes` / `import_graph` /
/// `flat_import_graph` / `module_fns` (pointers into maps owned by the
/// compilation driver, `core.zig`) — those are read-only views and are never
/// freed here.
/// `flat_import_graph` / `module_decls` / `namespace_edges` / `decl_table`
/// (pointers into maps owned by the compilation driver, `core.zig`) — those are
/// read-only views and are never freed here.
///
/// Per-map allocators are preserved exactly as they were on `Lowering`:
/// `import_flags` / `fn_ast_map` / `global_names` use the lowering allocator
@@ -615,11 +615,6 @@ pub const ProgramIndex = struct {
/// `ns :: #import`). fix-0102c's bare-name disambiguation walks this to
/// decide which same-name authors a flat importer can reach. Borrowed view.
flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null,
/// Module path → (function name → authoring `*const FnDecl`), mirroring
/// `module_scopes`. Retains every same-name author under its own path so
/// fix-0102c can resolve a flat call to the right module's function.
/// Borrowed view.
module_fns: ?*imports.ModuleFns = null,
/// Per-module scalar raw-decl index (`path → name → RawDeclRef`), built by
/// `imports.buildImportFacts`. The unified resolver's raw-fact store.
/// Borrowed view.
@@ -696,7 +691,7 @@ pub const ProgramIndex = struct {
pub fn deinit(self: *ProgramIndex) void {
// Owned maps only — module_scopes / import_graph / flat_import_graph /
// module_fns / module_decls / namespace_edges / decl_table are borrowed.
// module_decls / namespace_edges / decl_table are borrowed.
self.import_flags.deinit();
self.fn_ast_map.deinit();
self.qualified_fn_source.deinit();

View File

@@ -4,8 +4,7 @@
// resolveImports → buildImportFacts, the exact path core.zig drives) plus one
// synthetic diamond fixture for pointer-identity dedup. The visibility-adapter
// tests pin the nameVisibleOverEdges edge-walk that isNameVisible /
// isCImportVisible run on top of — including the user_bare_flat vs the
// over-permissive legacy_direct_any distinction.
// isCImportVisible run on top of — the flat-edge set vs the full import_graph.
const std = @import("std");
const ast = @import("../ast.zig");
@@ -82,8 +81,8 @@ fn tag(ref: resolver.RawDeclRef) std.meta.Tag(resolver.RawDeclRef) {
// ── collectVisibleAuthors ────────────────────────────────────────────────
// own author present; two distinct flat authors both returned RAW; and the
// user_bare_flat edge set EXCLUDES a namespaced-only import that the quarantined
// legacy_direct_any set still reaches.
// user_bare_flat edge set EXCLUDES a namespaced-only import (reachable only over
// a non-flat edge).
test "resolver: collectVisibleAuthors — own author, two distinct flat authors, namespaced edge excluded" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -129,16 +128,10 @@ test "resolver: collectVisibleAuthors — own author, two distinct flat authors,
try std.testing.expect(dup_set.flat[0].raw.fn_decl != dup_set.flat[1].raw.fn_decl);
// `secret` is authored only in p.sx, imported NAMESPACED (`g :: #import`).
// user_bare_flat must NOT see it (p.sx is not a flat edge)...
// user_bare_flat must NOT see it (p.sx is not a flat edge).
const flat_secret = r.collectVisibleAuthors("secret", main_path, .user_bare_flat);
try std.testing.expect(flat_secret.own == null);
try std.testing.expectEqual(@as(usize, 0), flat_secret.flat.len);
// ...but the quarantined legacy_direct_any set (import_graph) still reaches
// it — the exact over-permissiveness user_bare_flat tightens away.
const any_secret = r.collectVisibleAuthors("secret", main_path, .legacy_direct_any);
try std.testing.expect(any_secret.own == null);
try std.testing.expectEqual(@as(usize, 1), any_secret.flat.len);
}
// Diamond: the SAME author node is reachable over two flat edges. It must
@@ -234,10 +227,10 @@ test "resolver: collectNamespaceAuthors — returns target members, walks no gra
// ── visibility predicate (the isNameVisible / isCImportVisible core) ──────
// nameVisibleOverEdges is what isVisible(.user_bare_flat) (=> .flat graph) and
// the quarantined legacy_direct_any (=> import_graph) run on. They agree on own
// + flat names and differ ONLY on a namespaced-only name — the byte-identical
// behavior the adapters preserve vs the over-permissive set they avoid.
// nameVisibleOverEdges is the edge-walk isVisible(.user_bare_flat) runs on (the
// flat graph). Walked over the flat set vs the full import_graph, the two agree
// on own + flat names and differ ONLY on a namespaced-only name — the flat set
// the bare-name predicate uses, contrasted with the over-permissive full set.
test "resolver: visibility edge-walk — own + flat visible; namespaced-only only under import_graph" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -275,7 +268,7 @@ test "resolver: visibility edge-walk — own + flat visible; namespaced-only onl
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &all, "main", "dup"));
// Namespaced-only name: NOT visible under the flat set (user_bare_flat),
// but visible under the import_graph set (legacy_direct_any).
// but visible under the full import_graph set.
try std.testing.expect(!lower.nameVisibleOverEdges(&scopes, &flat, "main", "secret"));
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &all, "main", "secret"));

View File

@@ -3,8 +3,7 @@
//! A read-only facade over the borrowed Phase A import facts on a
//! `*ProgramIndex` (`module_decls` / `namespace_edges`) and the existing
//! `import_graph` / `flat_import_graph` views. It OWNS nothing import-derived;
//! those maps live in `imports.zig`/`core.zig` and are borrowed here, exactly
//! like `module_fns`.
//! those maps live in `imports.zig`/`core.zig` and are borrowed here.
//!
//! Two collectors sit on top of these facts (R5 §1 #1):
//! - `collectVisibleAuthors` — own author the flat-import edge walk. THE one
@@ -69,10 +68,6 @@ pub const VisibilityMode = enum {
/// Registration / lazy lowering: falls open (visible), emits no user
/// diagnostic, performs no graph walk.
lowering_internal,
/// own scope `import_graph` (flat AND namespaced edges) — an
/// over-permissive set. QUARANTINE: reserved for sites PROVEN to be internal
/// scans, never a user-facing lookup. Deleted in Phase K.
legacy_direct_any,
};
/// Read-only facade over the borrowed import facts. `alloc` backs the
@@ -91,10 +86,10 @@ pub const Resolver = struct {
/// this. `from` is the querying module's source path.
///
/// Edge set by mode: `flat_import_graph` for `user_bare_flat`/
/// `c_import_bare`; `import_graph` for the quarantined `legacy_direct_any`.
/// `impl_transitive` (a transitive closure owned by `findVisibleImpls`) and
/// `lowering_internal` (no graph walk) are not single-hop author walks —
/// reaching them here is a wiring bug, so we trip loudly.
/// `c_import_bare`. `impl_transitive` (a transitive closure owned by
/// `findVisibleImpls`) and `lowering_internal` (no graph walk) are not
/// single-hop author walks — reaching them here is a wiring bug, so we trip
/// loudly.
pub fn collectVisibleAuthors(
self: *Resolver,
name: []const u8,
@@ -111,7 +106,6 @@ pub const Resolver = struct {
const graph = (switch (vis) {
.user_bare_flat, .c_import_bare => self.index.flat_import_graph,
.legacy_direct_any => self.index.import_graph,
// findVisibleImpls owns transitive visibility; lowering_internal
// performs no graph walk. Neither selects a single-hop edge set.
.impl_transitive, .lowering_internal => @panic(