From cb9ef381b53a1402ca5a7e82c98964b98dc68b5d Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 15:22:10 +0300 Subject: [PATCH] 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. --- ...0768-modules-own-wins-nonleaf-bare-type.sx | 44 +++++++ .../dep.sx | 8 ++ .../0769-modules-ambiguous-type-fn-head.sx | 21 +++ .../0769-modules-ambiguous-type-fn-head/a.sx | 7 + .../0769-modules-ambiguous-type-fn-head/b.sx | 5 + ...68-modules-own-wins-nonleaf-bare-type.exit | 1 + ...-modules-own-wins-nonleaf-bare-type.stderr | 1 + ...-modules-own-wins-nonleaf-bare-type.stdout | 5 + .../0769-modules-ambiguous-type-fn-head.exit | 1 + ...0769-modules-ambiguous-type-fn-head.stderr | 5 + ...0769-modules-ambiguous-type-fn-head.stdout | 1 + readme.md | 13 +- src/ir/lower.zig | 122 ++++++++++++++---- 13 files changed, 203 insertions(+), 31 deletions(-) create mode 100644 examples/0768-modules-own-wins-nonleaf-bare-type.sx create mode 100644 examples/0768-modules-own-wins-nonleaf-bare-type/dep.sx create mode 100644 examples/0769-modules-ambiguous-type-fn-head.sx create mode 100644 examples/0769-modules-ambiguous-type-fn-head/a.sx create mode 100644 examples/0769-modules-ambiguous-type-fn-head/b.sx create mode 100644 examples/expected/0768-modules-own-wins-nonleaf-bare-type.exit create mode 100644 examples/expected/0768-modules-own-wins-nonleaf-bare-type.stderr create mode 100644 examples/expected/0768-modules-own-wins-nonleaf-bare-type.stdout create mode 100644 examples/expected/0769-modules-ambiguous-type-fn-head.exit create mode 100644 examples/expected/0769-modules-ambiguous-type-fn-head.stderr create mode 100644 examples/expected/0769-modules-ambiguous-type-fn-head.stdout diff --git a/examples/0768-modules-own-wins-nonleaf-bare-type.sx b/examples/0768-modules-own-wins-nonleaf-bare-type.sx new file mode 100644 index 0000000..d1f6533 --- /dev/null +++ b/examples/0768-modules-own-wins-nonleaf-bare-type.sx @@ -0,0 +1,44 @@ +// Own-wins (0754 / issue 0105 case 3) holds at EVERY non-leaf bare-type site, +// not just the nominal leaf annotation. `main` flat-imports `dep.sx` (which +// authors a same-name `Widget { a, b }` = 16 bytes and `Nums :: [2]s32` = 8 +// bytes) AND authors its OWN `Widget { a }` = 8 bytes and `Nums :: [2]s64` = 16 +// bytes. Each bare reference below must resolve to MAIN's own author — the gate's +// source-keyed `.resolved` author — NOT whichever same-name flat import a global +// `findByName` / `struct_template_map` pick would return: +// +// - reflection / type-arg slot `size_of(Widget)` → 8 (own) +// - typed array-literal head `Nums.[…]` / size_of → 16 (own [2]s64) +// - type-as-value `t : Type = Widget` +// - type-category match arm `case Widget:` → own identity +// +// Regression (Phase E4 attempt-6, finding #1): before the bare-type gate carried +// the OWN author's source-keyed TypeId into the non-leaf sites, an own author +// produced `.proceed` and these sites fell through to a global `findByName` — so +// `size_of(Widget)` printed the IMPORTED type's 16 and `case Widget:` matched the +// imported nominal identity (describe → 222). `dep_sizes` proves dep's distinct +// types still resolve to THEIR own 16 + 8 = 24 inside dep. + +#import "modules/std.sx"; +#import "0768-modules-own-wins-nonleaf-bare-type/dep.sx"; + +Widget :: struct { a: s64; } +Nums :: [2]s64; + +describe :: ($T: Type) -> s32 { + r := if T == { + case Widget: 111; + else: 222; + } + r +} + +main :: () -> s32 { + print("reflection={}\n", size_of(Widget)); + xs := Nums.[1, 2]; + print("array={}\n", size_of(Nums)); + t : Type = Widget; + print("typeval_eq={}\n", t == Widget); + print("match={}\n", describe(Widget)); + print("dep={}\n", dep_sizes()); + 0 +} diff --git a/examples/0768-modules-own-wins-nonleaf-bare-type/dep.sx b/examples/0768-modules-own-wins-nonleaf-bare-type/dep.sx new file mode 100644 index 0000000..45b36b4 --- /dev/null +++ b/examples/0768-modules-own-wins-nonleaf-bare-type/dep.sx @@ -0,0 +1,8 @@ +// A flat-imported module authors its OWN `Widget { a, b }` (16 bytes) and +// `Nums :: [2]s32` (8 bytes). The importing file (`main`) ALSO authors a +// same-name `Widget { a }` (8 bytes) and `Nums :: [2]s64` (16 bytes) — its own +// authors must win at EVERY bare-type site there (own-wins, 0754), while this +// module's distinct types stay live via `dep_sizes`. +Widget :: struct { a: s64; b: s64; } +Nums :: [2]s32; +dep_sizes :: () -> s64 { return size_of(Widget) + size_of(Nums); } diff --git a/examples/0769-modules-ambiguous-type-fn-head.sx b/examples/0769-modules-ambiguous-type-fn-head.sx new file mode 100644 index 0000000..af9e506 --- /dev/null +++ b/examples/0769-modules-ambiguous-type-fn-head.sx @@ -0,0 +1,21 @@ +// A type-returning FUNCTION head (`Make(s64)` where `Make :: ($T) -> Type`) is +// NON-transitive AND ambiguity-checked, exactly like the parameterized generic- +// struct / protocol heads (0767) and the nominal leaf (0755). `main` flat-imports +// two modules that each author a same-name `Make` type-fn with a different body +// and authors none itself, so the bare `Make(s64)` head is a genuine collision — +// it must emit the LOUD "type 'Make' is ambiguous" diagnostic and poison, NEVER +// silently instantiate whichever single author `fn_ast_map` happens to hold. +// +// Regression (Phase E4 attempt-6, finding #2): before `headFnLeak` did bare-call +// ambiguity selection it only checked `isNameVisible` (both authors ARE visible), +// so two flat `Make` type-fns silently instantiated one author — `size_of(Make(s64))` +// printed 16 (a.sx's two-field body) at exit 0 instead of the ambiguity diagnostic. + +#import "modules/std.sx"; +#import "0769-modules-ambiguous-type-fn-head/a.sx"; +#import "0769-modules-ambiguous-type-fn-head/b.sx"; + +main :: () -> s32 { + print("size={}\n", size_of(Make(s64))); + 0 +} diff --git a/examples/0769-modules-ambiguous-type-fn-head/a.sx b/examples/0769-modules-ambiguous-type-fn-head/a.sx new file mode 100644 index 0000000..a621761 --- /dev/null +++ b/examples/0769-modules-ambiguous-type-fn-head/a.sx @@ -0,0 +1,7 @@ +// One of two flat-imported authors of a same-name type-returning function +// `Make :: ($T) -> Type`. Its body returns a two-field struct; b.sx's returns a +// one-field struct, so a bare `Make(s64)` head is a genuine collision the +// importing source cannot disambiguate. +Make :: ($T: Type) -> Type { + return struct { x: T; y: T; }; +} diff --git a/examples/0769-modules-ambiguous-type-fn-head/b.sx b/examples/0769-modules-ambiguous-type-fn-head/b.sx new file mode 100644 index 0000000..d495e71 --- /dev/null +++ b/examples/0769-modules-ambiguous-type-fn-head/b.sx @@ -0,0 +1,5 @@ +// The second flat-imported author of same-name type-fn `Make`. Its distinct +// one-field body makes a bare `Make(s64)` head ambiguous against a.sx's. +Make :: ($T: Type) -> Type { + return struct { x: T; }; +} diff --git a/examples/expected/0768-modules-own-wins-nonleaf-bare-type.exit b/examples/expected/0768-modules-own-wins-nonleaf-bare-type.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0768-modules-own-wins-nonleaf-bare-type.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0768-modules-own-wins-nonleaf-bare-type.stderr b/examples/expected/0768-modules-own-wins-nonleaf-bare-type.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0768-modules-own-wins-nonleaf-bare-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0768-modules-own-wins-nonleaf-bare-type.stdout b/examples/expected/0768-modules-own-wins-nonleaf-bare-type.stdout new file mode 100644 index 0000000..2a4a972 --- /dev/null +++ b/examples/expected/0768-modules-own-wins-nonleaf-bare-type.stdout @@ -0,0 +1,5 @@ +reflection=8 +array=16 +typeval_eq=true +match=111 +dep=24 diff --git a/examples/expected/0769-modules-ambiguous-type-fn-head.exit b/examples/expected/0769-modules-ambiguous-type-fn-head.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0769-modules-ambiguous-type-fn-head.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0769-modules-ambiguous-type-fn-head.stderr b/examples/expected/0769-modules-ambiguous-type-fn-head.stderr new file mode 100644 index 0000000..70c2974 --- /dev/null +++ b/examples/expected/0769-modules-ambiguous-type-fn-head.stderr @@ -0,0 +1,5 @@ +error: type 'Make' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0769-modules-ambiguous-type-fn-head.sx:19:32 + | +19 | print("size={}\n", size_of(Make(s64))); + | ^^^^ diff --git a/examples/expected/0769-modules-ambiguous-type-fn-head.stdout b/examples/expected/0769-modules-ambiguous-type-fn-head.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0769-modules-ambiguous-type-fn-head.stdout @@ -0,0 +1 @@ + diff --git a/readme.md b/readme.md index 34881f7..1a36a0d 100644 --- a/readme.md +++ b/readme.md @@ -415,11 +415,14 @@ generic head) — is likewise not visible and is rejected (`type 'X' is not visi 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.[…]`), 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 +plain annotations. **Own-wins** holds at every one of those sites too, exactly like +a bare call: when the querying module declares its OWN same-name type, that bare +reference resolves to ITS author — never a same-name flat import. Ambiguity is +enforced at every one of those sites as well: a bare type (including a type-returning +function head) that two or more flat imports each declare — with no own author to +win — 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 e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call site.) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 49383c3..e965299 100644 --- a/src/ir/lower.zig +++ b/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