diff --git a/examples/0747-modules-namespaced-only-bare-alias-not-visible.sx b/examples/0747-modules-namespaced-only-bare-alias-not-visible.sx new file mode 100644 index 0000000..d1389a0 --- /dev/null +++ b/examples/0747-modules-namespaced-only-bare-alias-not-visible.sx @@ -0,0 +1,16 @@ +// Bare TYPE-ALIAS visibility under a NAMESPACED-only import — the alias sibling +// of 0743 (bare named type) and 0742 (bare const). A type ALIAS is a `const_decl` +// whose value resolved to a type, so it never registers a `findByName` named-type +// entry; the source-aware leaf must still treat it as a TYPE author (R4). `dep.sx` +// is imported only as `dep :: #import`, so its top-level `Secret :: s32` alias is +// reachable ONLY as `dep.Secret`. A BARE `Secret` in a type position must NOT +// resolve: bare-TYPE visibility joins over the FLAT import edges, and a namespaced +// alias is not a flat edge. Before the fix the ns-only alias was NOT a recognised +// type author, so the bare reference fell to the legacy empty-struct stub with NO +// diagnostic (the value silently came out 0). Regression (issue R4). +dep :: #import "0747-modules-namespaced-only-bare-alias-not-visible/dep.sx"; + +main :: () -> s32 { + x : Secret = 7; + x +} diff --git a/examples/0747-modules-namespaced-only-bare-alias-not-visible/dep.sx b/examples/0747-modules-namespaced-only-bare-alias-not-visible/dep.sx new file mode 100644 index 0000000..d6bda5f --- /dev/null +++ b/examples/0747-modules-namespaced-only-bare-alias-not-visible/dep.sx @@ -0,0 +1 @@ +Secret :: s32; diff --git a/examples/0748-modules-flat-alias-shadows-ns-only-type.sx b/examples/0748-modules-flat-alias-shadows-ns-only-type.sx new file mode 100644 index 0000000..fa075a6 --- /dev/null +++ b/examples/0748-modules-flat-alias-shadows-ns-only-type.sx @@ -0,0 +1,18 @@ +// A flat-visible (here own-module) type ALIAS must resolve even when a +// namespaced-only import authors a same-name NAMED type — the alias↔named-type +// analog of 0745/0746 (R4, FALSE-REJECTION direction). `dep.sx` is namespaced +// (`ns :: #import`) and authors a top-level `Secret` STRUCT; `main` authors its +// OWN top-level alias `Secret :: s32`. A bare `Secret` must resolve to MAIN's +// alias (`s32`), NOT be poisoned by the invisible same-name struct: the alias is +// the only flat-visible TYPE author. Before the fix the leaf saw the global +// `findByName` struct and, finding no NAMED-type author in `main` (an alias is a +// `const_decl`, not a named type), wrongly rejected the bare reference as "not +// visible". Regression (issue R4). +ns :: #import "0748-modules-flat-alias-shadows-ns-only-type/dep.sx"; + +Secret :: s32; + +main :: () -> s32 { + x : Secret = 42; + x +} diff --git a/examples/0748-modules-flat-alias-shadows-ns-only-type/dep.sx b/examples/0748-modules-flat-alias-shadows-ns-only-type/dep.sx new file mode 100644 index 0000000..6835705 --- /dev/null +++ b/examples/0748-modules-flat-alias-shadows-ns-only-type/dep.sx @@ -0,0 +1,4 @@ +Secret :: struct { + x: s32; + y: s32; +} diff --git a/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.exit b/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.stderr b/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.stderr new file mode 100644 index 0000000..af9756b --- /dev/null +++ b/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.stderr @@ -0,0 +1,5 @@ +error: type 'Secret' is not visible; #import the module that declares it + --> examples/0747-modules-namespaced-only-bare-alias-not-visible.sx:14:9 + | +14 | x : Secret = 7; + | ^^^^^^ diff --git a/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.stdout b/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0747-modules-namespaced-only-bare-alias-not-visible.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.exit b/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.exit new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.exit @@ -0,0 +1 @@ +42 diff --git a/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.stderr b/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.stdout b/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0748-modules-flat-alias-shadows-ns-only-type.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2b69f6e..a332dd7 100644 --- a/src/ir/lower.zig +++ b/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 :: `, 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 :: `, 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; }