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:
agra
2026-06-08 15:22:10 +03:00
parent 382f78f49b
commit cb9ef381b5
13 changed files with 203 additions and 31 deletions

View File

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