fix(stdlib/E4): carry full author outcome through the bare-TYPE gate (ambiguity at every site)

attempt-4 gated every bare-type-reference site for VISIBILITY via a boolean
leak-check that only caught not-visible and DROPPED the ambiguous outcome, so two
DIRECT flat same-name type authors (the 0755/0105 ambiguity case) fell through to
a global findByName / struct_template_map pick at the non-leaf sites.

Unified author-outcome fix (one path, every site consumes it):

- flatTypeAuthorCount: ≥2 distinct flat authors that do NOT all collapse onto one
  shared TypeId are now `.ambiguous` even when none carries a concrete TypeId yet —
  two same-name GENERIC TEMPLATES (template name registered in no findByName slot)
  are a genuine collision, exactly like two registered structs. Identical-target
  authors (diamond import / two aliases onto the same target) still collapse to
  `.one`, so all valid cases stay byte-identical.

- headTypeGate: the complete source-aware author outcome (.proceed / .resolved /
  .ambiguous / .not_visible) for an unqualified bare TYPE head, emitting the loud
  ambiguity diagnostic (consistent with the leaf / 0755) or the not-visible
  diagnostic. headTypeLeak is now its poison-vs-proceed projection, so every head /
  instantiation / alias-decl / match site poisons on ambiguity with the right
  message. Reflection / type-arg and array/vector-literal identifier heads consume
  `.resolved` to use the source-keyed TypeId, never a global findByName pick.

Regression examples/0767: size_of(Thing) / Nums.[1,2] / Box(s64) / t:Type=Thing /
case Thing: with two direct flat same-name authors each emit the ambiguity
diagnostic, exit 1 (fail-before on bb8f7dc: exit 0 / cascade). 0763/0764/0765/0766
/0755/0706/0544/0105 + FFI byte-identical. README: bare-type ambiguity is enforced
at every reference site.
This commit is contained in:
agra
2026-06-08 14:15:34 +03:00
parent bb8f7dc5ec
commit 382f78f49b
8 changed files with 209 additions and 48 deletions

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1 @@
1

View File

@@ -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;
| ^^^^^

View File

@@ -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 #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 / 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.[…]`), 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 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 e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call
site.) site.)

View File

@@ -2147,14 +2147,20 @@ pub const Lowering = struct {
/// (own-wins), so this surveys only the cross-module direct-flat authors: /// (own-wins), so this surveys only the cross-module direct-flat authors:
/// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4); /// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4);
/// - `.one` — exactly one distinct resolved TypeId; /// - `.one` — exactly one distinct resolved TypeId;
/// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId /// - `.unregistered` — exactly ONE flat author found and it does not resolve
/// yet (a forward reference, or a foreign/lazily-registered author with no /// to a TypeId yet (a forward reference, or a foreign/lazily-registered
/// `findByName` slot) → the caller yields the legacy stub, NOT a leak; /// 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 /// - `.none` — no flat author at all → the caller proceeds to the
/// local / leak / forward-alias arms. /// local / leak / forward-alias arms.
/// Distinctness is BY TypeId: each distinct author holds a distinct /// Distinctness is BY TypeId: each distinct author holds a distinct
/// `nominal_id` TypeId, while a diamond import of the SAME module yields the /// `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 /// INTERNAL bare-TYPE refs (a 2-flat-hop type like `List(T).append`'s
/// `alloc: Allocator`) stay resolvable because instantiation is source-pinned /// `alloc: Allocator`) stay resolvable because instantiation is source-pinned
/// to the template's defining module (E4 #1), so the query originates THERE — /// 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 graph = self.program_index.flat_import_graph orelse return .none;
const direct = graph.get(from) orelse return .none; const direct = graph.get(from) orelse return .none;
var found: ?TypeId = null; var found: ?TypeId = null;
var saw_author = false; var authors: usize = 0;
var tid_authors: usize = 0;
var it = direct.iterator(); var it = direct.iterator();
while (it.next()) |kv| { while (it.next()) |kv| {
const dep = kv.key_ptr.*; const dep = kv.key_ptr.*;
if (self.moduleTypeAuthor(dep, name) != null) { if (self.moduleTypeAuthor(dep, name) != null) {
saw_author = true; authors += 1;
if (self.moduleTypeAuthorTid(dep, name)) |tid| { if (self.moduleTypeAuthorTid(dep, name)) |tid| {
tid_authors += 1;
if (found) |f| { if (found) |f| {
if (tid != f) return .ambiguous; if (tid != f) return .ambiguous;
} else found = tid; } 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 }; 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 /// 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), .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span),
.identifier => |id| { .identifier => |id| {
// E4 single-hop visibility gate: a 2-flat-hop bare type name in a // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type
// typed array/vector-literal annotation (`Nums.[1, 2]`) is not // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is
// bare-visible (consistent with annotations / 0763). // not bare-visible (consistent with annotations / 0763); ≥2 direct
if (self.headTypeLeak(id.name, te.span)) return .unresolved; // 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); const name_id = self.module.types.internString(id.name);
return self.module.types.findByName(name_id) orelse .unresolved; return self.module.types.findByName(name_id) orelse .unresolved;
}, },
@@ -12410,12 +12435,20 @@ pub const Lowering = struct {
if (self.type_bindings) |tb| { if (self.type_bindings) |tb| {
if (tb.get(id.name)) |ty| return ty; if (tb.get(id.name)) |ty| return ty;
} }
// E4 single-hop visibility gate: a bare type name reachable only // E4 single-hop visibility + ambiguity gate: a bare type name
// over 2+ flat hops is not bare-visible in a reflection / type-arg // reachable only over 2+ flat hops is not bare-visible in a
// slot either (consistent with normal annotations / 0763). A // reflection / type-arg slot (consistent with normal annotations /
// genuinely-undeclared name is NOT authored as a type anywhere, so // 0763); ≥2 direct flat same-name authors are ambiguous (loud
// the gate falls through to the "unresolved type" diagnostic below. // diagnostic, consistent with the leaf / 0755) instead of a global
if (self.headTypeLeak(id.name, node.span)) return .unresolved; // 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; if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
const name_id = self.module.types.internString(id.name); const name_id = self.module.types.internString(id.name);
if (self.module.types.findByName(name_id)) |t| return t; 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 }); 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 /// The poison-vs-proceed projection of `headTypeGate` for an UNQUALIFIED
/// type HEAD that names a generic STRUCT or a parameterized PROTOCOL /// parameterized type HEAD that names a generic STRUCT, a parameterized
/// (`Box(s64)`, `VL(s64)`) — the constructor-head analog of the bare-leaf /// PROTOCOL, or a type-returning function used as a head (`Box(s64)`,
/// type gate (E4). A head is visible iff a TYPE author for `name` is reachable /// `VL(s64)`) — and the alias-registration / type-match sites that likewise
/// from the USE site over its OWN declaration or a DIRECT flat-import edge — /// only need "poison or proceed". Returns TRUE (the gate's loud diagnostic is
/// the SAME single-hop set the bare leaf / value / fn leaves use (0706), NOT /// already emitted) when the head is `.not_visible` (a 2-flat-hop leak) or
/// the transitive closure. Emits the leak diagnostic + returns TRUE when the /// `.ambiguous` (≥2 direct flat same-name authors — consistent with the leaf /
/// head is a real type author somewhere but NOT reachable here (a 2-flat-hop /// 0755); FALSE when it resolves or falls open. See `headTypeGate` for the full
/// leak), so the caller poisons with `.unresolved`. Falls open (FALSE, no /// non-transitive visibility + ambiguity model and the fall-open conditions.
/// 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).
fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
if (self.emitting_default_context) return false; // A head site INSTANTIATES (template / type-fn) rather than substituting a
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false; // nominal TypeId, so it consumes only the poison-vs-proceed bit of the
const from = self.current_source_file orelse return false; // full author outcome: `.ambiguous` / `.not_visible` (loud diagnostic
// Reachable as a TYPE author over own / direct-flat edges → visible. // already emitted by `headTypeGate`) poison; `.resolved` / `.proceed`
if (self.moduleTypeAuthor(from, name) != null) return false; // proceed to instantiation.
if (self.flatTypeAuthorCount(name, from) != .none) return false; return switch (self.headTypeGate(name, span)) {
// A block-local generic declared in THIS source is visible here. .ambiguous, .not_visible => true,
if (self.localTypeInSource(from, name)) return false; .proceed, .resolved => false,
// Authored as a TYPE somewhere but unreachable from `from` → a leak. };
if (!self.nameAuthoredAsTypeAnywhere(name)) return 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| if (self.diagnostics) |d|
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); 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 /// Single-hop non-transitive visibility gate for an UNQUALIFIED type-returning