fix(stdlib/E4): own-wins at non-leaf bare-type sites + type-fn head ambiguity
attempt-6: address Adi's two in-scope findings (#3 deferred to E6). #1 E4-own-author-type-arg (silent-wrong): the bare-TYPE gate returned `.proceed` for the querying source's OWN author, so the non-leaf sites (reflection / type-arg / array-literal / type-value / match arm) dropped it and re-resolved a same-name flat import via global `findByName`. headTypeGate now resolves the own author to ITS per-source TypeId (mirroring selectNominalLeaf's own-wins, 0754); the type-as-value and type-match sites, which only consumed the poison bit and re-resolved globally, now route through the gate and use the `.resolved` author. size_of(Widget) with an own + imported Widget now yields main's own size, not the import's. #2 E4-type-fn-head-ambiguity (silent-wrong): headFnLeak only checked isNameVisible, so two flat same-name type-returning functions both reported "visible" and one was silently instantiated. It now diagnoses >=2 distinct direct flat type-fn authors (no own author) as ambiguous before the isNameVisible short-circuit, consistent with the parameterized struct / protocol heads and the leaf (0755/0767). Own / single / diamond-collapse type-fn heads still resolve. Regressions: 0768 (own-wins at every non-leaf bare-type site, fail-before reflection=16 -> pass-after 8) and 0769 (two flat Make type-fns -> ambiguity diagnostic exit 1). README: own-wins + type-fn-head ambiguity at every bare-type site.
This commit is contained in:
122
src/ir/lower.zig
122
src/ir/lower.zig
@@ -4115,13 +4115,21 @@ pub const Lowering = struct {
|
||||
// (`x: Type = Vec4`), comparison (`x == Vec4`), and
|
||||
// pack-arg / Any context (boxing happens at the
|
||||
// consumer).
|
||||
// E4 single-hop visibility gate: a bare type name used as a VALUE
|
||||
// (`x: Type = COnly`, `x == COnly`) reachable only over 2+ flat hops
|
||||
// is not bare-visible either (consistent with annotations / 0763).
|
||||
// `headTypeLeak` fires only for a real type author unreachable from
|
||||
// here; a value name / generic param / undeclared name falls through.
|
||||
if (self.headTypeLeak(id.name, node.span)) break :blk self.emitPlaceholder(id.name);
|
||||
// E4 single-hop visibility + ambiguity gate: a bare type name used
|
||||
// as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over
|
||||
// 2+ flat hops is not bare-visible (consistent with annotations /
|
||||
// 0763); ≥2 direct flat same-name authors are ambiguous (loud
|
||||
// diagnostic, 0755/0767). A single source-keyed author — including
|
||||
// the querying source's OWN author over a same-name flat import
|
||||
// (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name
|
||||
// author a global `findByName` would pick. A value name / generic
|
||||
// param / undeclared name → `.proceed`, falling through below.
|
||||
const ty = blk_ty: {
|
||||
switch (self.headTypeGate(id.name, node.span)) {
|
||||
.ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name),
|
||||
.resolved => |tid| break :blk_ty tid,
|
||||
.proceed => {},
|
||||
}
|
||||
if (self.type_bindings) |tb| {
|
||||
if (tb.get(id.name)) |t| break :blk_ty t;
|
||||
}
|
||||
@@ -5449,15 +5457,28 @@ pub const Lowering = struct {
|
||||
.type_expr => |te| te.name,
|
||||
else => "",
|
||||
};
|
||||
// E4 single-hop visibility gate: a SPECIFIC 2-flat-hop type name in
|
||||
// a type-match arm (`case COnly:`) is not bare-visible (consistent
|
||||
// with annotations / 0763). A category keyword (`int`, `struct`, …)
|
||||
// is not a type author anywhere, so the gate is a no-op for those.
|
||||
if (self.headTypeLeak(name, pat.span)) {
|
||||
arm_tag_values.append(self.alloc, &.{}) catch unreachable;
|
||||
continue;
|
||||
}
|
||||
const tag_values = self.resolveTypeCategoryTags(name);
|
||||
// E4 single-hop visibility + ambiguity gate: a SPECIFIC 2-flat-hop
|
||||
// type name in a type-match arm (`case COnly:`) is not bare-visible
|
||||
// (consistent with annotations / 0763); ≥2 direct flat same-name
|
||||
// authors are ambiguous (loud diagnostic, 0755/0767). A category
|
||||
// keyword (`int`, `struct`, …) is not a type author anywhere → the
|
||||
// gate is a no-op (`.proceed`) and `resolveTypeCategoryTags` expands
|
||||
// it. A source-keyed specific TYPE author — including the querying
|
||||
// source's OWN author over a same-name flat import (own-wins, 0754) —
|
||||
// matches on ITS TypeId, NOT whichever same-name author a global
|
||||
// `findByName` (inside `resolveTypeCategoryTags`) would pick.
|
||||
const tag_values = switch (self.headTypeGate(name, pat.span)) {
|
||||
.ambiguous, .not_visible => {
|
||||
arm_tag_values.append(self.alloc, &.{}) catch unreachable;
|
||||
continue;
|
||||
},
|
||||
.resolved => |tid| blk_tv: {
|
||||
const tv = self.alloc.alloc(u64, 1) catch unreachable;
|
||||
tv[0] = tid.index();
|
||||
break :blk_tv tv;
|
||||
},
|
||||
.proceed => self.resolveTypeCategoryTags(name),
|
||||
};
|
||||
arm_tag_values.append(self.alloc, tag_values) catch unreachable;
|
||||
for (tag_values) |tag| {
|
||||
cases.append(self.alloc, .{
|
||||
@@ -13969,8 +13990,22 @@ pub const Lowering = struct {
|
||||
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;
|
||||
// The querying source's OWN author wins outright (own-wins, 0754) — even
|
||||
// against a same-name flat import. Resolve to ITS per-source TypeId so a
|
||||
// NON-leaf bare-type site (reflection / type-arg / array-literal /
|
||||
// type-value / match arm) uses the own author, NOT whichever same-name
|
||||
// flat author a global `findByName` would pick. Mirrors
|
||||
// `selectNominalLeaf`'s own-author arm. A not-yet-registered own author (a
|
||||
// forward / self reference resolved mid-registration, or a generic
|
||||
// template the head path instantiates) carries no concrete TypeId yet →
|
||||
// `.proceed`, so the existing instantiation / stub path handles it.
|
||||
if (self.moduleTypeAuthor(from, name)) |author| switch (author) {
|
||||
.alias => |tid| return .{ .resolved = tid },
|
||||
.named => |ref| {
|
||||
if (self.namedRefTid(ref, name)) |tid| return .{ .resolved = tid };
|
||||
return .proceed;
|
||||
},
|
||||
};
|
||||
switch (self.flatTypeAuthorCount(name, from)) {
|
||||
.none => {},
|
||||
.one => |tid| return .{ .resolved = tid },
|
||||
@@ -13993,24 +14028,59 @@ pub const Lowering = struct {
|
||||
return .not_visible;
|
||||
}
|
||||
|
||||
/// Single-hop non-transitive visibility gate for an UNQUALIFIED type-returning
|
||||
/// FUNCTION head used as a type (`Make(N, T)` where `Make :: ($K, $T) -> Type`).
|
||||
/// A type-fn is a `fn_decl`, so its visibility is FUNCTION visibility
|
||||
/// (`isNameVisible`, the single-hop flat name set) — NOT the type-author model.
|
||||
/// Emits + returns TRUE when the fn is authored somewhere but not reachable
|
||||
/// from the use site (a 2-flat-hop leak). A scope-local (mangled) type-fn is
|
||||
/// visible in its own scope and exempt; falls open when unwired / default-
|
||||
/// context. Diagnostic mirrors the type form (the head IS used as a type here).
|
||||
/// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED
|
||||
/// type-returning FUNCTION head used as a type (`Make(N, T)` where
|
||||
/// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so its visibility is
|
||||
/// FUNCTION visibility (`isNameVisible`, the single-hop flat name set) — NOT the
|
||||
/// type-author model. Returns TRUE (loud diagnostic already emitted) when the
|
||||
/// head is AMBIGUOUS (≥2 distinct direct flat same-name type-fn authors, no own
|
||||
/// author — consistent with the parameterized struct / protocol heads and the
|
||||
/// leaf, 0755/0767, never a silent `fn_ast_map` first-/last-wins pick) or
|
||||
/// NOT-VISIBLE (authored somewhere but unreachable from the use site, a
|
||||
/// 2-flat-hop leak). A scope-local (mangled) type-fn or the querying source's
|
||||
/// OWN author wins outright (own-wins) and is exempt; falls open when unwired /
|
||||
/// default-context. Diagnostic mirrors the type form (the head IS used as a type
|
||||
/// here).
|
||||
fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
|
||||
if (self.emitting_default_context) return false;
|
||||
if (self.current_source_file == null) return false;
|
||||
const from = self.current_source_file orelse return false;
|
||||
if (self.scope) |s| if (s.lookupFn(name) != null) return false;
|
||||
// ≥2 distinct direct flat type-fn authors with no own author — a genuine
|
||||
// collision the source cannot disambiguate. Diagnose loudly BEFORE the
|
||||
// `isNameVisible` short-circuit, which would otherwise report "visible" and
|
||||
// let the single `fn_ast_map[name]` author silently win.
|
||||
if (self.flatFnAuthorAmbiguous(name, from)) {
|
||||
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 true;
|
||||
}
|
||||
if (self.isNameVisible(name)) return false;
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// TRUE iff bare `name` has ≥2 DISTINCT direct flat-import authors that are
|
||||
/// type-returning FUNCTIONS (`fn_decl`s — `fnDeclOfRaw` unwraps a const-wrapped
|
||||
/// fn) and the querying source authors NONE itself. The querying source's OWN
|
||||
/// author wins outright (own-wins), so an own author short-circuits to "not
|
||||
/// ambiguous" — the existing single-author path instantiates it. Diamond
|
||||
/// imports of the SAME author collapse in `collectVisibleAuthors`'s
|
||||
/// author-identity de-dup, so two edges onto one type-fn are NOT ambiguous. The
|
||||
/// type-fn ambiguity analogue of `flatTypeAuthorCount`'s `.ambiguous` for named
|
||||
/// type / template heads.
|
||||
fn flatFnAuthorAmbiguous(self: *Lowering, name: []const u8, from: []const u8) bool {
|
||||
var res = self.resolver();
|
||||
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
|
||||
defer if (set.flat.len > 0) self.alloc.free(set.flat);
|
||||
if (set.own != null) return false; // own-wins
|
||||
var fn_authors: usize = 0;
|
||||
for (set.flat) |fa| {
|
||||
if (fnDeclOfRaw(fa.raw) != null) fn_authors += 1;
|
||||
}
|
||||
return fn_authors >= 2;
|
||||
}
|
||||
|
||||
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
|
||||
fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
|
||||
// A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is
|
||||
|
||||
Reference in New Issue
Block a user