wip(resolver): collectors + unified predicate + tightened adapters [stdlib B, BLOCKED on 0106]
Collectors (resolver.zig: collectVisibleAuthors/collectNamespaceAuthors + AuthorSet + VisibilityMode, 4 unit tests) + unified visibility predicate + isNameVisible/ isCImportVisible adapters routed to flat modes. Tightening surfaces issue 0106 (stdlib comptime expansion relies on the over-permissive import_graph join), so run_examples is 467/471 here. attempt-2 folds in the coupled comptime-context fix.
This commit is contained in:
117
src/ir/lower.zig
117
src/ir/lower.zig
@@ -12,6 +12,7 @@ const interp_mod = @import("interp.zig");
|
||||
const errors = @import("../errors.zig");
|
||||
const jni_descriptor = @import("jni_descriptor.zig");
|
||||
const program_index_mod = @import("program_index.zig");
|
||||
const resolver_mod = @import("resolver.zig");
|
||||
const ProgramIndex = program_index_mod.ProgramIndex;
|
||||
const GlobalInfo = program_index_mod.GlobalInfo;
|
||||
const StructTemplate = program_index_mod.StructTemplate;
|
||||
@@ -110,6 +111,30 @@ const CleanupEntry = struct {
|
||||
binding: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Pure non-transitive visibility walk: `name` is visible from `source` when
|
||||
/// it's in `source`'s own scope or in any module reachable over one `graph`
|
||||
/// edge. The core of the lowering visibility predicate, exposed so a unit test
|
||||
/// can exercise the edge-walk without standing up a whole `Lowering`. Falls open
|
||||
/// (true) when `scopes`/`graph` are null (scoping infra unwired).
|
||||
pub fn nameVisibleOverEdges(
|
||||
scopes: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
source: []const u8,
|
||||
name: []const u8,
|
||||
) bool {
|
||||
const sc = scopes orelse return true;
|
||||
const own_scope = sc.get(source) orelse return true;
|
||||
if (own_scope.contains(name)) return true;
|
||||
const g = graph orelse return true;
|
||||
const direct = g.get(source) orelse return true;
|
||||
var it = direct.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const dep = sc.get(kv.key_ptr.*) orelse continue;
|
||||
if (dep.contains(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Lowering ────────────────────────────────────────────────────────────
|
||||
|
||||
pub const Lowering = struct {
|
||||
@@ -1765,45 +1790,71 @@ pub const Lowering = struct {
|
||||
// null-FuncId path (`lowerFunction`), which runs after all types resolve.
|
||||
}
|
||||
|
||||
/// The unified non-transitive `#import` visibility predicate, parameterized
|
||||
/// by `VisibilityMode`. `isNameVisible` / `isCImportVisible` are thin
|
||||
/// adapters over it.
|
||||
///
|
||||
/// This is the lowering-side GATE: it walks `module_scopes` (the per-file
|
||||
/// name set) joined over the edge set the mode selects. It is distinct from
|
||||
/// `resolver.collectVisibleAuthors`, which collects raw AUTHORS over
|
||||
/// `module_decls` — the single graph-walk that lives in `resolver.zig`. The
|
||||
/// two read different facts (name set vs author refs) for different jobs, so
|
||||
/// the gate's own iterator stays here, not in the resolver.
|
||||
///
|
||||
/// `module_scopes[F]` holds ONLY the names authored in F (plus its namespace
|
||||
/// aliases); cross-module visibility is joined here at query time. Doing the
|
||||
/// join at lookup (instead of pre-merging in `resolveImports`) lets cyclic
|
||||
/// imports like std.sx ↔ allocators.sx still resolve, since the cycle's
|
||||
/// skipped edge is still recorded in the graph and the partner's scope is
|
||||
/// filled in by the time lowering queries it.
|
||||
fn isVisible(self: *Lowering, name: []const u8, vis: resolver_mod.VisibilityMode) bool {
|
||||
switch (vis) {
|
||||
// Registration / lazy lowering paths don't police user visibility.
|
||||
.lowering_internal => return true,
|
||||
// Transitive visibility is ProtocolResolver.findVisibleImpls' job;
|
||||
// this predicate is single-hop only.
|
||||
.impl_transitive => @panic("isVisible: transitive visibility is owned by findVisibleImpls"),
|
||||
.c_import_bare => {
|
||||
// Foreign-C gate: only C-import fn_decls without a library_ref
|
||||
// are policed; a non-foreign body or a library-bound foreign
|
||||
// decl is unconditionally visible.
|
||||
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);
|
||||
},
|
||||
.user_bare_flat => return self.visibleOverEdges(name, .flat),
|
||||
.legacy_direct_any => return self.visibleOverEdges(name, .all),
|
||||
}
|
||||
}
|
||||
|
||||
const VisEdgeSet = enum { flat, all };
|
||||
|
||||
/// Resolve the mode's edge set and run the per-file visibility walk. 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 {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Check if a C-imported function is visible from the current source file.
|
||||
/// Returns true for non-C functions (always visible) or if no scoping info available.
|
||||
/// Returns true for non-C functions (always visible) or if no scoping info
|
||||
/// available. Byte-identical adapter over `isVisible`.
|
||||
fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool {
|
||||
const fd = self.program_index.fn_ast_map.get(fn_name) orelse return true;
|
||||
// Only restrict C import fn_decls: foreign_expr with no library_ref
|
||||
if (fd.body.data != .foreign_expr) return true;
|
||||
if (fd.body.data.foreign_expr.library_ref != null) return true;
|
||||
return self.isNameVisible(fn_name);
|
||||
return self.isVisible(fn_name, .c_import_bare);
|
||||
}
|
||||
|
||||
/// Non-transitive `#import` visibility check for top-level decls.
|
||||
///
|
||||
/// `module_scopes[F]` holds ONLY the names authored in file F (plus its
|
||||
/// namespace aliases). Cross-module visibility is joined here at query
|
||||
/// time by walking each direct flat-import edge in `import_graph` — a
|
||||
/// name is visible from F when it's authored in F or in any module F
|
||||
/// directly `#import`s. Doing the join here (instead of pre-merging in
|
||||
/// `resolveImports`) lets cyclic imports like std.sx ↔ allocators.sx
|
||||
/// still resolve, since the cycle's skipped edge is still recorded in
|
||||
/// `import_graph` and the partner's scope is filled in by the time
|
||||
/// lowering queries it.
|
||||
///
|
||||
/// Falls open when the scoping infrastructure isn't wired (comptime
|
||||
/// callers, directory imports without main_file, etc.). The caller is
|
||||
/// responsible for restricting the call to names that ARE known
|
||||
/// top-level decls; otherwise every local variable would be policed.
|
||||
/// Byte-identical adapter over `isVisible`.
|
||||
fn isNameVisible(self: *Lowering, name: []const u8) bool {
|
||||
const scopes = self.program_index.module_scopes orelse return true;
|
||||
const source = self.current_source_file orelse return true;
|
||||
const own_scope = scopes.get(source) orelse return true;
|
||||
if (own_scope.contains(name)) return true;
|
||||
const graph = self.program_index.import_graph orelse return true;
|
||||
const direct = graph.get(source) orelse return true;
|
||||
var it = direct.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const dep = scopes.get(kv.key_ptr.*) orelse continue;
|
||||
if (dep.contains(name)) return true;
|
||||
}
|
||||
return false;
|
||||
return self.isVisible(name, .user_bare_flat);
|
||||
}
|
||||
|
||||
/// Lazily lower a function body on demand. Called when lowerCall can't find
|
||||
|
||||
Reference in New Issue
Block a user