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

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