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:
167
src/ir/lower.zig
167
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
|
||||
|
||||
Reference in New Issue
Block a user