diff --git a/examples/0767-modules-ambiguous-bare-type-forms.sx b/examples/0767-modules-ambiguous-bare-type-forms.sx new file mode 100644 index 0000000..5d817fb --- /dev/null +++ b/examples/0767-modules-ambiguous-bare-type-forms.sx @@ -0,0 +1,39 @@ +// Bare-TYPE references are NON-transitive AND ambiguity-checked at every site, +// not just the nominal leaf annotation (0755). `main` flat-imports two modules +// that each author a same-name `Thing` / `Box` / `Nums` and authors none itself, +// so EACH of the following bare forms is a genuine collision the source cannot +// disambiguate — and each must emit the LOUD "type ... is ambiguous" diagnostic +// (consistent with the leaf, 0755) and poison the result, NEVER silently pick a +// global `findByName` / `struct_template_map` author: +// +// - reflection / type-arg slot `size_of(Thing)` +// - typed array/vector-literal `Nums.[1, 2]` +// - parameterized generic head `Box(s64)` +// - type-as-value `t : Type = Thing` +// - type-category match arm `case Thing:` +// +// Regression (Phase E4 attempt-5): before the bare-type gate carried the full +// source-aware author outcome, these non-leaf sites used a boolean leak-check +// that dropped the AMBIGUOUS outcome — two direct flat same-name authors fell +// through to a global pick (exit 0 / cascade) instead of the loud diagnostic. + +#import "modules/std.sx"; +#import "0767-modules-ambiguous-bare-type-forms/a.sx"; +#import "0767-modules-ambiguous-bare-type-forms/b.sx"; + +describe :: ($T: Type) -> s32 { + r := if T == { + case Thing: 1; + else: 0; + } + r +} + +main :: () -> s32 { + sz := size_of(Thing); + xs := Nums.[1, 2]; + x : Box(s64) = .{ v = 3 }; + t : Type = Thing; + d := describe(s64); + 0 +} diff --git a/examples/0767-modules-ambiguous-bare-type-forms/a.sx b/examples/0767-modules-ambiguous-bare-type-forms/a.sx new file mode 100644 index 0000000..41387e4 --- /dev/null +++ b/examples/0767-modules-ambiguous-bare-type-forms/a.sx @@ -0,0 +1,6 @@ +// One of two flat-imported authors of same-name types `Thing` / `Box` / `Nums`. +// With both modules flat-visible from a file that authors none itself, every +// bare reference to these names is genuinely ambiguous. +Thing :: struct { a: s64; } +Box :: struct($T: Type) { v: T; } +Nums :: [2]s64; diff --git a/examples/0767-modules-ambiguous-bare-type-forms/b.sx b/examples/0767-modules-ambiguous-bare-type-forms/b.sx new file mode 100644 index 0000000..8502874 --- /dev/null +++ b/examples/0767-modules-ambiguous-bare-type-forms/b.sx @@ -0,0 +1,7 @@ +// The second flat-imported author of same-name `Thing` / `Box` / `Nums`. The +// distinct shapes (`Thing` a separate nominal identity, `Box` a separate generic +// template, `Nums` aliased to a different element width) make each bare +// reference a real collision the importing source cannot disambiguate. +Thing :: struct { a: s64; } +Box :: struct($T: Type) { v: T; } +Nums :: [2]s32; diff --git a/examples/expected/0767-modules-ambiguous-bare-type-forms.exit b/examples/expected/0767-modules-ambiguous-bare-type-forms.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0767-modules-ambiguous-bare-type-forms.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0767-modules-ambiguous-bare-type-forms.stderr b/examples/expected/0767-modules-ambiguous-bare-type-forms.stderr new file mode 100644 index 0000000..42a400b --- /dev/null +++ b/examples/expected/0767-modules-ambiguous-bare-type-forms.stderr @@ -0,0 +1,29 @@ +error: type 'Thing' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0767-modules-ambiguous-bare-type-forms.sx:33:19 + | +33 | sz := size_of(Thing); + | ^^^^^ + +error: type 'Nums' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0767-modules-ambiguous-bare-type-forms.sx:34:11 + | +34 | xs := Nums.[1, 2]; + | ^^^^ + +error: type 'Box' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0767-modules-ambiguous-bare-type-forms.sx:35:9 + | +35 | x : Box(s64) = .{ v = 3 }; + | ^^^^^^^^ + +error: type 'Thing' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0767-modules-ambiguous-bare-type-forms.sx:36:16 + | +36 | t : Type = Thing; + | ^^^^^ + +error: type 'Thing' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0767-modules-ambiguous-bare-type-forms.sx:26:14 + | +26 | case Thing: 1; + | ^^^^^ diff --git a/examples/expected/0767-modules-ambiguous-bare-type-forms.stdout b/examples/expected/0767-modules-ambiguous-bare-type-forms.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0767-modules-ambiguous-bare-type-forms.stdout @@ -0,0 +1 @@ + diff --git a/readme.md b/readme.md index 2b399ad..34881f7 100644 --- a/readme.md +++ b/readme.md @@ -414,7 +414,12 @@ generic head) — is likewise not visible and is rejected (`type 'X' is not visi #import the module that declares it`); qualify it as `m.name`. The type gate holds wherever a bare type name is named — a value/field annotation, a reflection / type-arg slot (`size_of(T)`, `size_of(*T)`), a typed array-literal head (`T.[…]`), -or a type-as-value / type-match arm — not just plain annotations. (A library's own *internal* type references still resolve: a generic +a parameterized head (`Box(s64)`), or a type-as-value / type-match arm — not just +plain annotations. Ambiguity is enforced at every one of those sites too, exactly +like a bare call: a bare type that two or more flat imports each declare is +**ambiguous and rejected** (`type 'X' is ambiguous: it is declared in multiple +flat-imported modules; qualify the reference or remove the duplicate import`) — never +a silent pick of one author. (A library's own *internal* type references still resolve: a generic struct / pack fn / protocol body is instantiated in the module that defines it, so e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call site.) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 7a7798a..49383c3 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -2147,14 +2147,20 @@ pub const Lowering = struct { /// (own-wins), so this surveys only the cross-module direct-flat authors: /// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4); /// - `.one` — exactly one distinct resolved TypeId; - /// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId - /// yet (a forward reference, or a foreign/lazily-registered author with no - /// `findByName` slot) → the caller yields the legacy stub, NOT a leak; + /// - `.unregistered` — exactly ONE flat author found and it does not resolve + /// to a TypeId yet (a forward reference, or a foreign/lazily-registered + /// author with no `findByName` slot) → the caller yields the legacy stub, + /// NOT a leak; /// - `.none` — no flat author at all → the caller proceeds to the /// local / leak / forward-alias arms. /// Distinctness is BY TypeId: each distinct author holds a distinct /// `nominal_id` TypeId, while a diamond import of the SAME module yields the - /// same TypeId, so byte-identical de-dup falls out. A library template's + /// same TypeId, so byte-identical de-dup falls out. ≥2 distinct flat authors + /// that do NOT all collapse onto one shared TypeId are `.ambiguous` even when + /// none carries a concrete TypeId yet — two same-name GENERIC TEMPLATES (whose + /// template name is registered in no `findByName` slot) are a genuine + /// collision the source cannot disambiguate, exactly like two registered + /// structs (issue 0105 case 4). A library template's /// INTERNAL bare-TYPE refs (a 2-flat-hop type like `List(T).append`'s /// `alloc: Allocator`) stay resolvable because instantiation is source-pinned /// to the template's defining module (E4 #1), so the query originates THERE — @@ -2166,21 +2172,33 @@ pub const Lowering = struct { const graph = self.program_index.flat_import_graph orelse return .none; const direct = graph.get(from) orelse return .none; var found: ?TypeId = null; - var saw_author = false; + var authors: usize = 0; + var tid_authors: usize = 0; var it = direct.iterator(); while (it.next()) |kv| { const dep = kv.key_ptr.*; if (self.moduleTypeAuthor(dep, name) != null) { - saw_author = true; + authors += 1; if (self.moduleTypeAuthorTid(dep, name)) |tid| { + tid_authors += 1; if (found) |f| { if (tid != f) return .ambiguous; } else found = tid; } } } + if (authors == 0) return .none; + // ≥2 distinct flat authors that do NOT all collapse onto a single shared + // TypeId are genuinely ambiguous: two same-name GENERIC TEMPLATES (neither + // carries a concrete TypeId, `tid_authors == 0`), a registered author + // colliding with a same-name forward / template author (`tid_authors < + // authors`), or — caught above by the per-TypeId early return — two + // distinct registered TypeIds. Only when EVERY author resolved to ONE + // shared TypeId (a diamond import, or two aliases onto the same target) + // does it collapse to `.one`. + if (authors >= 2 and !(tid_authors == authors and found != null)) return .ambiguous; if (found) |t| return .{ .one = t }; - return if (saw_author) .unregistered else .none; + return .unregistered; } /// TRUE iff `name` is authored as a TYPE — a NAMED type OR a type ALIAS — in @@ -6876,10 +6894,17 @@ pub const Lowering = struct { }, .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), .identifier => |id| { - // E4 single-hop visibility gate: a 2-flat-hop bare type name in a - // typed array/vector-literal annotation (`Nums.[1, 2]`) is not - // bare-visible (consistent with annotations / 0763). - if (self.headTypeLeak(id.name, te.span)) return .unresolved; + // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type + // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is + // not bare-visible (consistent with annotations / 0763); ≥2 direct + // flat same-name authors are ambiguous (loud diagnostic, consistent + // with the leaf / 0755); a single source-keyed author resolves to + // ITS TypeId instead of a global `findByName` first-/last-wins pick. + switch (self.headTypeGate(id.name, te.span)) { + .ambiguous, .not_visible => return .unresolved, + .resolved => |tid| return tid, + .proceed => {}, + } const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .unresolved; }, @@ -12410,12 +12435,20 @@ pub const Lowering = struct { if (self.type_bindings) |tb| { if (tb.get(id.name)) |ty| return ty; } - // E4 single-hop visibility gate: a bare type name reachable only - // over 2+ flat hops is not bare-visible in a reflection / type-arg - // slot either (consistent with normal annotations / 0763). A - // genuinely-undeclared name is NOT authored as a type anywhere, so - // the gate falls through to the "unresolved type" diagnostic below. - if (self.headTypeLeak(id.name, node.span)) return .unresolved; + // E4 single-hop visibility + ambiguity gate: a bare type name + // reachable only over 2+ flat hops is not bare-visible in a + // reflection / type-arg slot (consistent with normal annotations / + // 0763); ≥2 direct flat same-name authors are ambiguous (loud + // diagnostic, consistent with the leaf / 0755) instead of a global + // first-/last-wins pick; a single source-keyed author resolves to + // ITS TypeId. A genuinely-undeclared name is NOT authored as a type + // anywhere → `.proceed`, falling to the "unresolved type" + // diagnostic below. + switch (self.headTypeGate(id.name, node.span)) { + .ambiguous, .not_visible => return .unresolved, + .resolved => |tid| return tid, + .proceed => {}, + } if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty; const name_id = self.module.types.internString(id.name); if (self.module.types.findByName(name_id)) |t| return t; @@ -13885,39 +13918,79 @@ pub const Lowering = struct { d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name }); } - /// Single-hop non-transitive visibility gate for an UNQUALIFIED parameterized - /// type HEAD that names a generic STRUCT or a parameterized PROTOCOL - /// (`Box(s64)`, `VL(s64)`) — the constructor-head analog of the bare-leaf - /// type gate (E4). A head is visible iff a TYPE author for `name` is reachable - /// from the USE site over its OWN declaration or a DIRECT flat-import edge — - /// the SAME single-hop set the bare leaf / value / fn leaves use (0706), NOT - /// the transitive closure. Emits the leak diagnostic + returns TRUE when the - /// head is a real type author somewhere but NOT reachable here (a 2-flat-hop - /// leak), so the caller poisons with `.unresolved`. Falls open (FALSE, no - /// diagnostic) when import facts are unwired (registration / comptime — no - /// querying module), the source context is absent, or the compiler-synthesized - /// default-Context emitter is running (built-in infrastructure resolves - /// independent of the user program's import style, F1). A block-local generic - /// of THIS source is visible in its own scope. Library-internal heads stay - /// visible because every instantiation kind is source-pinned to the template's - /// defining module (E3/E4 #1): the query originates THERE, where the head is a - /// direct flat import — not at the cross-module call site. Only the bare - /// (identifier-callee / dotless) form is gated; a namespaced `ns.Box(..)` head - /// is an explicit qualified reach and is exempt (the caller skips this gate). + /// The poison-vs-proceed projection of `headTypeGate` for an UNQUALIFIED + /// parameterized type HEAD that names a generic STRUCT, a parameterized + /// PROTOCOL, or a type-returning function used as a head (`Box(s64)`, + /// `VL(s64)`) — and the alias-registration / type-match sites that likewise + /// only need "poison or proceed". Returns TRUE (the gate's loud diagnostic is + /// already emitted) when the head is `.not_visible` (a 2-flat-hop leak) or + /// `.ambiguous` (≥2 direct flat same-name authors — consistent with the leaf / + /// 0755); FALSE when it resolves or falls open. See `headTypeGate` for the full + /// non-transitive visibility + ambiguity model and the fall-open conditions. fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { - if (self.emitting_default_context) return false; - if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false; - const from = self.current_source_file orelse return false; - // Reachable as a TYPE author over own / direct-flat edges → visible. - if (self.moduleTypeAuthor(from, name) != null) return false; - if (self.flatTypeAuthorCount(name, from) != .none) return false; - // A block-local generic declared in THIS source is visible here. - if (self.localTypeInSource(from, name)) return false; - // Authored as a TYPE somewhere but unreachable from `from` → a leak. - if (!self.nameAuthoredAsTypeAnywhere(name)) return false; + // A head site INSTANTIATES (template / type-fn) rather than substituting a + // nominal TypeId, so it consumes only the poison-vs-proceed bit of the + // full author outcome: `.ambiguous` / `.not_visible` (loud diagnostic + // already emitted by `headTypeGate`) poison; `.resolved` / `.proceed` + // proceed to instantiation. + return switch (self.headTypeGate(name, span)) { + .ambiguous, .not_visible => true, + .proceed, .resolved => false, + }; + } + + /// The complete source-aware author outcome of an UNQUALIFIED bare TYPE head — + /// the unified non-transitive visibility + ambiguity gate every bare-type- + /// reference site OUTSIDE the nominal leaf routes through (E4 attempt-5): + /// reflection / type-arg slots, typed array/vector-literal heads, parameterized + /// generic / protocol / type-fn heads, type-as-value, and type-category match + /// arms. Mirrors `selectNominalLeaf`'s author model so a 2-flat-hop type is + /// `.not_visible`, ≥2 direct flat same-name authors are `.ambiguous` (the LOUD + /// diagnostic, consistent with the leaf / 0755 — never a silent global + /// `findByName` / `struct_template_map` first-/last-wins pick), and a single + /// direct flat author resolves to ITS source-keyed TypeId. Falls open + /// (`.proceed`) when import facts are unwired, the source context is absent, + /// the default-Context emitter is running (built-in infrastructure resolves + /// independent of the user's import style, F1), the querying source is the OWN + /// author, a single flat author is not registered yet (a forward / foreign / + /// generic template — the caller instantiates it), or `name` is a block-local + /// of this source / no type author at all. Library-internal heads stay visible + /// because every instantiation kind is source-pinned to the template's defining + /// module (E3/E4 #1): the query originates THERE, where the head is a direct + /// flat import. A namespaced `ns.Box(..)` head is an explicit qualified reach + /// and is exempt (the caller skips this gate). + const HeadTypeGate = union(enum) { + proceed, + resolved: TypeId, + ambiguous, + not_visible, + }; + fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate { + if (self.emitting_default_context) return .proceed; + if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return .proceed; + const from = self.current_source_file orelse return .proceed; + // The querying source's OWN author binds through the existing path. + if (self.moduleTypeAuthor(from, name) != null) return .proceed; + switch (self.flatTypeAuthorCount(name, from)) { + .none => {}, + .one => |tid| return .{ .resolved = tid }, + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); + return .ambiguous; + }, + // A single flat author exists but its TypeId is not registered yet (a + // forward reference, a foreign / lazily-registered class, or a generic + // template) — fall open so the caller instantiates / stubs it. + .unregistered => return .proceed, + } + // A block-local type / generic declared in THIS source is visible here. + if (self.localTypeInSource(from, name)) return .proceed; + // Not a cross-module type author at all → nothing to gate. + if (!self.nameAuthoredAsTypeAnywhere(name)) return .proceed; if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); - return true; + return .not_visible; } /// Single-hop non-transitive visibility gate for an UNQUALIFIED type-returning