fix(resolver): treat type aliases as bare-TYPE authors in both gate directions [stdlib E1 attempt-4]
R4: a type alias is a `const_decl`, not a named-type decl, so the bare-TYPE
visibility gate ignored aliases — a namespaced-only alias leaked bare (silent
empty-struct stub, no diagnostic) and a flat-visible alias was poisoned by an
invisible same-name named type. Unify both type-author kinds (named type AND
alias) behind one per-module predicate `moduleTypeAuthor`, returning the author
KIND so resolution is decoupled from `findByName` timing (a forward/self
reference like `next: *ArenaChunk`, unregistered mid-registration, is still
recognised as an author and falls to the legacy stub instead of a false
"not visible"). The leak detector `nameAuthoredAsTypeAnywhere` now also scans
`type_aliases_by_source`. Single source of truth across named types, top-level
aliases, and parameterized/type-fn aliases — leak side and false-rejection side.
Behavior-preserving for single-author names (full suite byte-identical, paths
normalized). Generic / parameterized-protocol / Vector / type-function heads
stay legacy (0210). Block-local `Name :: <type>` remains a value const under the
reserved-name duality (pre-existing; the gate handles it safely, no leak).
Regressions: 0747 (ns-only alias bare -> not visible), 0748 (flat-visible alias
not poisoned by ns-only same-name struct). Both fail-before on 4bd57c8 /
pass-after here.
This commit is contained in:
258
src/ir/lower.zig
258
src/ir/lower.zig
@@ -1788,65 +1788,103 @@ pub const Lowering = struct {
|
||||
if (name.len > 0 and (name[0] == '[' or name[0] == '*' or name[0] == '?')) {
|
||||
return .{ .resolved = self.typeResolver().resolveName(name, raw) };
|
||||
}
|
||||
// Bare nominal name: select its author over the ONE graph-walk collector
|
||||
// (`typeBareVisibleAsType` — flat-import reachable TYPE author) plus the
|
||||
// single-hop collector for the alias path. A bare TYPE name is visible iff
|
||||
// a flat-import-reachable module authors it AS A TYPE (R1: a same-name
|
||||
// VALUE/FUNCTION does not count); a namespaced-only TYPE is registered
|
||||
// GLOBALLY yet reachable only over a namespace edge, so without this gate
|
||||
// its bare reference leaked through `findByName`'s global first-match.
|
||||
// Bare nominal name. A bare TYPE name is visible iff a flat-import-
|
||||
// reachable module authors it AS A TYPE — and a TYPE author is EITHER a
|
||||
// named type (struct/enum/union/error-set/protocol/foreign class) OR a
|
||||
// type ALIAS (`Name :: <type>`, a `const_decl` whose value resolved to a
|
||||
// type, recorded in E0's `type_aliases_by_source`). Both kinds are gated
|
||||
// identically: `moduleTypeAuthor` is the SINGLE source of truth, so a
|
||||
// namespaced-only alias leaks no more than a namespaced-only named type,
|
||||
// and a flat-visible alias is never poisoned by an invisible same-name
|
||||
// named type (and vice-versa) — R4. A same-name flat VALUE/FUNCTION is
|
||||
// NOT a type author (R1); a value-const (`N :: 7`) lives in
|
||||
// `module_consts_by_source`, never in `type_aliases_by_source`, so it is
|
||||
// correctly excluded too.
|
||||
//
|
||||
// The TYPE reachability here is the TRANSITIVE flat-import closure, NOT the
|
||||
// single-hop `collectVisibleAuthors`/`isNameVisible` set the bare VALUE /
|
||||
// FUNCTION / CONST leaves use. That asymmetry (types transitive, values
|
||||
// non-transitive — 0706) is the open model-consistency question (R3, E1):
|
||||
// the value/function model needs the source pin for a library template's
|
||||
// INTERNAL type refs (`List.append`'s `alloc: Allocator`, instantiated in
|
||||
// the caller's source context) before the type gate can go single-hop too
|
||||
// — see the worker report. Until that lands, the transitive type closure
|
||||
// is the only byte-identical option; the gate stays type-author-aware and
|
||||
// local-safe regardless of which reachability E1.x settles on.
|
||||
// non-transitive — 0706) is the open model-consistency question (R3,
|
||||
// sequenced as E4 per Agra): the value/function model needs the source pin
|
||||
// for a library template's INTERNAL type refs (`List.append`'s
|
||||
// `alloc: Allocator`, instantiated in the caller's source context) before
|
||||
// the type gate can go single-hop too. Until that lands, the transitive
|
||||
// type closure is the only byte-identical option; the gate stays
|
||||
// type-author-aware and local-safe regardless of which reachability E4
|
||||
// settles on.
|
||||
const name_id = table.internString(name);
|
||||
const registered = table.findByName(name_id);
|
||||
|
||||
// Registered named type (struct/enum/union/error_set/protocol/foreign
|
||||
// class) — gated on flat-import TYPE visibility (F1, the type analog of
|
||||
// Phase B's value/function tightening).
|
||||
if (registered) |existing| {
|
||||
// Compiler-synthesized default-Context emission resolves the built-in
|
||||
// allocator types as infrastructure — fall open (the gate is for USER
|
||||
// bare references, not compiler internals).
|
||||
if (self.emitting_default_context) return .{ .resolved = existing };
|
||||
// A flat-import-reachable TYPE author makes the bare reference visible.
|
||||
// The author must be a TYPE — a same-name flat VALUE/FUNCTION does NOT
|
||||
// make a namespaced-only type bare-visible (R1). Single-author (E1): the
|
||||
// unique `findByName` match IS that author's TypeId, so a bare-visible
|
||||
// name resolves byte-identically; E2 routes this through the collector-
|
||||
// selected author's per-source `nominal_id` once same-name type shadows
|
||||
// register.
|
||||
if (self.typeBareVisibleAsType(name, from)) return .{ .resolved = existing };
|
||||
// A block-local type (declared inside a fn / init body) clobbers the
|
||||
// global entry for its name, so `existing` IS that local type — never a
|
||||
// namespaced-only leak. Resolve it ungated (R2): a legitimately-scoped
|
||||
// local must not be rejected just because a namespaced-only import also
|
||||
// authors a top-level type of the same name.
|
||||
if (self.local_type_names.contains(name)) return .{ .resolved = existing };
|
||||
// Registered as a TOP-LEVEL named TYPE in some module, but NOT flat-
|
||||
// import-reachable from `from` and NOT shadowed by a local → reachable
|
||||
// only over a namespace edge → leak. Return `.not_visible`;
|
||||
// `resolveNominalLeaf` surfaces the diagnostic and the `.unresolved`
|
||||
// sentinel (qualify it `ns.Type`, Phase F).
|
||||
if (self.nameAuthoredAsTypeAnywhere(name)) return .not_visible;
|
||||
// Not a top-level type author anywhere — a generic type-param's bound
|
||||
// or a fabricated empty-struct stub. Not a bare cross-module reference;
|
||||
// resolve ungated (its own diagnostics still fire in the dedicated pass).
|
||||
return .{ .resolved = existing };
|
||||
// Compiler-synthesized default-Context emission resolves the built-in
|
||||
// allocator types as infrastructure — fall open (the gate is for USER bare
|
||||
// references, not compiler internals).
|
||||
if (self.emitting_default_context) {
|
||||
if (registered) |existing| return .{ .resolved = existing };
|
||||
}
|
||||
// Type alias `A :: B`. Select the alias author over the single-hop
|
||||
// collector and read its target from the source-keyed cache, keyed by the
|
||||
// author's OWN declaring source (E0's write side) — this is where the
|
||||
// global-alias-leak (0104-F2) fix begins, replacing the global
|
||||
// `type_alias_map` first-match for a flat-visible alias.
|
||||
// Import facts unwired (registration / comptime host with no module_decls
|
||||
// or flat graph): there is no querying context to gate against — preserve
|
||||
// the legacy resolution (registered → existing; else forward-alias /
|
||||
// undeclared).
|
||||
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) {
|
||||
if (registered) |existing| return .{ .resolved = existing };
|
||||
return self.forwardAliasOrUndeclared(name, from);
|
||||
}
|
||||
|
||||
// 1. A flat-import-visible TYPE author (named type OR alias) — resolve to
|
||||
// its declared TypeId. Single-author (E1): at most one author across
|
||||
// own ∪ flat closure, so this is byte-identical to the legacy leaf.
|
||||
// The author KIND decides resolution, decoupled from `findByName`
|
||||
// timing: an ALIAS resolves to its `type_aliases_by_source` target; a
|
||||
// NAMED type resolves to its `findByName` entry, OR — when that entry
|
||||
// is not registered yet (a forward / self reference like
|
||||
// `next: *ArenaChunk` resolved mid-registration) — to the legacy
|
||||
// empty-struct stub, reconciled by `updatePreservingKey` when the type
|
||||
// finally registers. E2 routes named resolution through the
|
||||
// collector-selected author's per-source `nominal_id` once same-name
|
||||
// type shadows register.
|
||||
if (self.flatVisibleTypeAuthor(name, from)) |author| switch (author) {
|
||||
.alias => |tid| return .{ .resolved = tid },
|
||||
.named => {
|
||||
if (registered) |existing| return .{ .resolved = existing };
|
||||
return .undeclared;
|
||||
},
|
||||
};
|
||||
|
||||
// 2. A block-local type (declared inside a fn / init body) clobbers the
|
||||
// global entry for its name, so `existing` IS that local type — never a
|
||||
// namespaced-only leak. Resolve it ungated (R2): a legitimately-scoped
|
||||
// local must not be rejected just because a namespaced-only import also
|
||||
// authors a top-level type of the same name.
|
||||
if (self.local_type_names.contains(name)) {
|
||||
if (registered) |existing| return .{ .resolved = existing };
|
||||
}
|
||||
|
||||
// 3. Authored as a TYPE (named OR alias) in some module, but NOT flat-
|
||||
// import-reachable from `from` and NOT shadowed by a local → reachable
|
||||
// only over a namespace edge → leak. Return `.not_visible`;
|
||||
// `resolveNominalLeaf` surfaces the diagnostic and the `.unresolved`
|
||||
// sentinel (qualify it `ns.Type`, Phase F).
|
||||
if (self.nameAuthoredAsTypeAnywhere(name)) return .not_visible;
|
||||
|
||||
// 4. Not a cross-module type author. A registered generic type-param bound
|
||||
// or fabricated empty-struct stub (findByName hit, no module_decls
|
||||
// author) resolves ungated. Otherwise a forward identifier alias
|
||||
// (visible const author, target not resolved yet → `.pending`, back to
|
||||
// the fixpoint) or `.undeclared`.
|
||||
if (registered) |existing| return .{ .resolved = existing };
|
||||
return self.forwardAliasOrUndeclared(name, from);
|
||||
}
|
||||
|
||||
/// The forward-alias / undeclared tail of `selectNominalLeaf`: a bare nominal
|
||||
/// name that is neither a flat-visible type author, a local, nor a leak.
|
||||
/// Selects the single-hop const author (E1: `collectVisibleAuthors` returns
|
||||
/// ≤1) and, if its alias target is not yet in `type_aliases_by_source`,
|
||||
/// returns `.pending` so the forward-alias fixpoint re-resolves it (source-
|
||||
/// aware in E1.5). A resolved flat-visible alias is already returned by
|
||||
/// `flatVisibleTypeAuthor` above, so the `inner.get` here only catches a
|
||||
/// const author reachable via `collectVisibleAuthors` whose target landed
|
||||
/// between the two reads — the fixpoint path is the common outcome.
|
||||
fn forwardAliasOrUndeclared(self: *Lowering, name: []const u8, from: []const u8) TypeHeadResolution {
|
||||
var res = self.resolver();
|
||||
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
|
||||
defer if (set.flat.len > 0) self.alloc.free(set.flat);
|
||||
@@ -1854,8 +1892,6 @@ pub const Lowering = struct {
|
||||
if (self.program_index.type_aliases_by_source.get(author.source)) |inner| {
|
||||
if (inner.get(name)) |alias_ty| return .{ .resolved = alias_ty };
|
||||
}
|
||||
// Const author visible but its alias target is not resolved yet —
|
||||
// a forward identifier alias → `.pending`, back to the fixpoint.
|
||||
return .pending;
|
||||
}
|
||||
return .undeclared;
|
||||
@@ -1872,9 +1908,10 @@ pub const Lowering = struct {
|
||||
|
||||
/// TRUE iff `raw` declares a NAMED TYPE — struct / enum / union / error-set /
|
||||
/// protocol / foreign class. A `fn_decl`, a value-or-alias `const_decl`, and a
|
||||
/// `namespace_decl` are NOT named types (a type alias is a `const_decl` whose
|
||||
/// `findByName` lookup fails, so it never reaches the named-type gate; it
|
||||
/// resolves through the alias path keyed by the const author instead).
|
||||
/// `namespace_decl` are NOT named types. A type ALIAS is a `const_decl`; it is
|
||||
/// recognised as a type author NOT here but via `type_aliases_by_source`
|
||||
/// (E0's source-keyed cache) in `moduleTypeAuthor`, so the two type-author
|
||||
/// kinds — named type and alias — gate identically (R4).
|
||||
fn isNamedTypeKind(raw: resolver_mod.RawDeclRef) bool {
|
||||
return switch (raw) {
|
||||
.struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => true,
|
||||
@@ -1882,39 +1919,64 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// TRUE iff module `path` authors a top-level decl named `name` AS A TYPE
|
||||
/// (struct/enum/union/error-set/protocol/foreign class). A same-name
|
||||
/// VALUE/FUNCTION author returns false (R1: the gate is type-author-aware, not
|
||||
/// name-only) — the per-module leaf of `typeBareVisibleAsType`'s closure walk.
|
||||
fn moduleAuthorsType(decls: *imports_mod.ModuleDecls, path: []const u8, name: []const u8) bool {
|
||||
const m = decls.get(path) orelse return false;
|
||||
const ref = m.names.get(name) orelse return false;
|
||||
return isNamedTypeKind(ref);
|
||||
/// A module's authorship of a bare type `name`: an ALIAS (carrying the
|
||||
/// resolved target `TypeId` from `type_aliases_by_source`) or a NAMED type
|
||||
/// (struct/enum/union/error-set/protocol/foreign class — its TypeId is
|
||||
/// resolved at the use site from `findByName`, decoupled from this predicate
|
||||
/// so a not-yet-registered forward / self reference is still recognised as an
|
||||
/// author).
|
||||
const FlatTypeAuthor = union(enum) {
|
||||
alias: TypeId,
|
||||
named,
|
||||
};
|
||||
|
||||
/// How module `path` authors `name` AS A TYPE, or null if it does not. A type
|
||||
/// author is EITHER a type ALIAS (`Name :: <type>`, recorded in E0's
|
||||
/// `type_aliases_by_source` — checked first via the source-keyed cache) OR a
|
||||
/// NAMED type (recognised by its `module_decls` decl KIND, NOT by `findByName`
|
||||
/// — so a forward / self reference resolved before the type registers is still
|
||||
/// an author). A same-name VALUE/FUNCTION is NOT a type author (R1); a
|
||||
/// value-const (`N :: 7`) lives in `module_consts_by_source`, never
|
||||
/// `type_aliases_by_source`, so it returns null too. THE per-module "is `name`
|
||||
/// a type author here?" predicate — the single source of truth for the
|
||||
/// visibility walk (R4).
|
||||
fn moduleTypeAuthor(self: *Lowering, path: []const u8, name: []const u8) ?FlatTypeAuthor {
|
||||
if (self.program_index.type_aliases_by_source.get(path)) |inner| {
|
||||
if (inner.get(name)) |tid| return .{ .alias = tid };
|
||||
}
|
||||
const decls = self.program_index.module_decls orelse return null;
|
||||
const m = decls.get(path) orelse return null;
|
||||
const ref = m.names.get(name) orelse return null;
|
||||
if (!isNamedTypeKind(ref)) return null;
|
||||
return .named;
|
||||
}
|
||||
|
||||
/// TRUE iff bare `name` is reachable from `from` AS A TYPE over the flat-import
|
||||
/// closure (own decls ∪ every transitively flat-imported module's own decls).
|
||||
/// This is the type-author-aware analog of the value/function visibility, but
|
||||
/// TRANSITIVE rather than single-hop — the open R3 asymmetry (see the gate
|
||||
/// comment in `selectNominalLeaf`): a library template's INTERNAL type ref
|
||||
/// (`List.append`'s `alloc: Allocator`, declared in `std.sx` but instantiated
|
||||
/// in the caller's source context) is two flat hops from the caller, and the
|
||||
/// value/function source-pin that would let the type gate go single-hop too is
|
||||
/// not yet wired for generic instantiation. A same-name flat VALUE/FUNCTION does
|
||||
/// NOT make the name type-visible (R1). The closure walk lives in `lower.zig`,
|
||||
/// NOT `resolver.zig`, so the single-graph-walk invariant (one
|
||||
/// `flat_import_graph` iterator in `resolver.zig`) is untouched. Falls open
|
||||
/// (visible) when the scoping facts are unwired (comptime / registration).
|
||||
fn typeBareVisibleAsType(self: *Lowering, name: []const u8, from: []const u8) bool {
|
||||
const decls = self.program_index.module_decls orelse return true;
|
||||
const graph = self.program_index.flat_import_graph orelse return true;
|
||||
if (moduleAuthorsType(decls, from, name)) return true;
|
||||
/// The flat-import-visible TYPE author for bare `name` from `from`, or null
|
||||
/// when no flat-reachable module authors `name` as a type. Walks own decls ∪
|
||||
/// the TRANSITIVE flat-import closure (every transitively flat-imported
|
||||
/// module), returning the first author found via `moduleTypeAuthor` (named
|
||||
/// type OR alias — the single source of truth, R4). Single-author (E1): at
|
||||
/// most one author across the closure, so the first hit IS the unique author.
|
||||
///
|
||||
/// TRANSITIVE rather than single-hop is the open R3 asymmetry (types
|
||||
/// transitive, values non-transitive — 0706; sequenced as E4 per Agra): a
|
||||
/// library template's INTERNAL type ref (`List.append`'s `alloc: Allocator`,
|
||||
/// declared in `std.sx` but instantiated in the caller's source context) is
|
||||
/// two flat hops from the caller, and the value/function source-pin that would
|
||||
/// let the type gate go single-hop too is not yet wired for generic
|
||||
/// instantiation. The closure walk lives in `lower.zig`, NOT `resolver.zig`,
|
||||
/// so the single-graph-walk invariant (one `flat_import_graph` iterator in
|
||||
/// `resolver.zig`) is untouched. Returns null when the flat graph is unwired
|
||||
/// (the caller has already special-cased that to the legacy resolution).
|
||||
fn flatVisibleTypeAuthor(self: *Lowering, name: []const u8, from: []const u8) ?FlatTypeAuthor {
|
||||
const graph = self.program_index.flat_import_graph orelse return null;
|
||||
if (self.moduleTypeAuthor(from, name)) |a| return a;
|
||||
var visited = std.StringHashMap(void).init(self.alloc);
|
||||
defer visited.deinit();
|
||||
var queue = std.ArrayList([]const u8).empty;
|
||||
defer queue.deinit(self.alloc);
|
||||
visited.put(from, {}) catch return true;
|
||||
queue.append(self.alloc, from) catch return true;
|
||||
visited.put(from, {}) catch return null;
|
||||
queue.append(self.alloc, from) catch return null;
|
||||
var i: usize = 0;
|
||||
while (i < queue.items.len) : (i += 1) {
|
||||
const deps = graph.get(queue.items[i]) orelse continue;
|
||||
@@ -1923,24 +1985,32 @@ pub const Lowering = struct {
|
||||
const dep = kv.key_ptr.*;
|
||||
if (visited.contains(dep)) continue;
|
||||
visited.put(dep, {}) catch continue;
|
||||
if (moduleAuthorsType(decls, dep, name)) return true;
|
||||
if (self.moduleTypeAuthor(dep, name)) |a| return a;
|
||||
queue.append(self.alloc, dep) catch continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// TRUE iff `name` is authored as a TOP-LEVEL NAMED TYPE in ANY module's raw
|
||||
/// facts. Distinguishes a real cross-module TYPE author (the only thing the
|
||||
/// bare-flat visibility gate polices) from a LOCAL type / generic-param /
|
||||
/// TRUE iff `name` is authored as a TYPE — a NAMED type OR a type ALIAS — in
|
||||
/// ANY module's raw facts. The leak detector: a name that is a type author
|
||||
/// somewhere but not flat-visible from the querying module is reachable only
|
||||
/// over a namespace edge. Both kinds are checked (R4): named types via
|
||||
/// `module_decls`, aliases via E0's `type_aliases_by_source`. Distinguishes a
|
||||
/// real cross-module TYPE author from a LOCAL type / generic-param /
|
||||
/// fabricated empty-struct stub (findByName-registered but authored in no
|
||||
/// `module_decls`) and from a same-name VALUE/FUNCTION author (not a type).
|
||||
/// Unwired facts → false (nothing to gate; resolve ungated).
|
||||
/// module) and from a same-name VALUE/FUNCTION author (not a type). Unwired
|
||||
/// facts → false (nothing to gate; resolve ungated).
|
||||
fn nameAuthoredAsTypeAnywhere(self: *Lowering, name: []const u8) bool {
|
||||
const decls = self.program_index.module_decls orelse return false;
|
||||
var it = decls.valueIterator();
|
||||
while (it.next()) |m| {
|
||||
if (m.names.get(name)) |ref| if (isNamedTypeKind(ref)) return true;
|
||||
if (self.program_index.module_decls) |decls| {
|
||||
var it = decls.valueIterator();
|
||||
while (it.next()) |m| {
|
||||
if (m.names.get(name)) |ref| if (isNamedTypeKind(ref)) return true;
|
||||
}
|
||||
}
|
||||
var ait = self.program_index.type_aliases_by_source.valueIterator();
|
||||
while (ait.next()) |inner| {
|
||||
if (inner.contains(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user