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:
44
examples/0768-modules-own-wins-nonleaf-bare-type.sx
Normal file
44
examples/0768-modules-own-wins-nonleaf-bare-type.sx
Normal file
@@ -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
|
||||||
|
}
|
||||||
8
examples/0768-modules-own-wins-nonleaf-bare-type/dep.sx
Normal file
8
examples/0768-modules-own-wins-nonleaf-bare-type/dep.sx
Normal file
@@ -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); }
|
||||||
21
examples/0769-modules-ambiguous-type-fn-head.sx
Normal file
21
examples/0769-modules-ambiguous-type-fn-head.sx
Normal file
@@ -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
|
||||||
|
}
|
||||||
7
examples/0769-modules-ambiguous-type-fn-head/a.sx
Normal file
7
examples/0769-modules-ambiguous-type-fn-head/a.sx
Normal file
@@ -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; };
|
||||||
|
}
|
||||||
5
examples/0769-modules-ambiguous-type-fn-head/b.sx
Normal file
5
examples/0769-modules-ambiguous-type-fn-head/b.sx
Normal file
@@ -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; };
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
reflection=8
|
||||||
|
array=16
|
||||||
|
typeval_eq=true
|
||||||
|
match=111
|
||||||
|
dep=24
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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)));
|
||||||
|
| ^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
13
readme.md
13
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 /
|
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.[…]`),
|
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
|
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
|
plain annotations. **Own-wins** holds at every one of those sites too, exactly like
|
||||||
like a bare call: a bare type that two or more flat imports each declare is
|
a bare call: when the querying module declares its OWN same-name type, that bare
|
||||||
**ambiguous and rejected** (`type 'X' is ambiguous: it is declared in multiple
|
reference resolves to ITS author — never a same-name flat import. Ambiguity is
|
||||||
flat-imported modules; qualify the reference or remove the duplicate import`) — never
|
enforced at every one of those sites as well: a bare type (including a type-returning
|
||||||
a silent pick of one author. (A library's own *internal* type references still resolve: a generic
|
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
|
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
|
e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call
|
||||||
site.)
|
site.)
|
||||||
|
|||||||
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
|
// (`x: Type = Vec4`), comparison (`x == Vec4`), and
|
||||||
// pack-arg / Any context (boxing happens at the
|
// pack-arg / Any context (boxing happens at the
|
||||||
// consumer).
|
// consumer).
|
||||||
// E4 single-hop visibility gate: a bare type name used as a VALUE
|
// E4 single-hop visibility + ambiguity gate: a bare type name used
|
||||||
// (`x: Type = COnly`, `x == COnly`) reachable only over 2+ flat hops
|
// as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over
|
||||||
// is not bare-visible either (consistent with annotations / 0763).
|
// 2+ flat hops is not bare-visible (consistent with annotations /
|
||||||
// `headTypeLeak` fires only for a real type author unreachable from
|
// 0763); ≥2 direct flat same-name authors are ambiguous (loud
|
||||||
// here; a value name / generic param / undeclared name falls through.
|
// diagnostic, 0755/0767). A single source-keyed author — including
|
||||||
if (self.headTypeLeak(id.name, node.span)) break :blk self.emitPlaceholder(id.name);
|
// 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: {
|
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 (self.type_bindings) |tb| {
|
||||||
if (tb.get(id.name)) |t| break :blk_ty t;
|
if (tb.get(id.name)) |t| break :blk_ty t;
|
||||||
}
|
}
|
||||||
@@ -5449,15 +5457,28 @@ pub const Lowering = struct {
|
|||||||
.type_expr => |te| te.name,
|
.type_expr => |te| te.name,
|
||||||
else => "",
|
else => "",
|
||||||
};
|
};
|
||||||
// E4 single-hop visibility gate: a SPECIFIC 2-flat-hop type name in
|
// E4 single-hop visibility + ambiguity gate: a SPECIFIC 2-flat-hop
|
||||||
// a type-match arm (`case COnly:`) is not bare-visible (consistent
|
// type name in a type-match arm (`case COnly:`) is not bare-visible
|
||||||
// with annotations / 0763). A category keyword (`int`, `struct`, …)
|
// (consistent with annotations / 0763); ≥2 direct flat same-name
|
||||||
// is not a type author anywhere, so the gate is a no-op for those.
|
// authors are ambiguous (loud diagnostic, 0755/0767). A category
|
||||||
if (self.headTypeLeak(name, pat.span)) {
|
// keyword (`int`, `struct`, …) is not a type author anywhere → the
|
||||||
arm_tag_values.append(self.alloc, &.{}) catch unreachable;
|
// gate is a no-op (`.proceed`) and `resolveTypeCategoryTags` expands
|
||||||
continue;
|
// it. A source-keyed specific TYPE author — including the querying
|
||||||
}
|
// source's OWN author over a same-name flat import (own-wins, 0754) —
|
||||||
const tag_values = self.resolveTypeCategoryTags(name);
|
// 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;
|
arm_tag_values.append(self.alloc, tag_values) catch unreachable;
|
||||||
for (tag_values) |tag| {
|
for (tag_values) |tag| {
|
||||||
cases.append(self.alloc, .{
|
cases.append(self.alloc, .{
|
||||||
@@ -13969,8 +13990,22 @@ pub const Lowering = struct {
|
|||||||
if (self.emitting_default_context) return .proceed;
|
if (self.emitting_default_context) return .proceed;
|
||||||
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) 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;
|
const from = self.current_source_file orelse return .proceed;
|
||||||
// The querying source's OWN author binds through the existing path.
|
// The querying source's OWN author wins outright (own-wins, 0754) — even
|
||||||
if (self.moduleTypeAuthor(from, name) != null) return .proceed;
|
// 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)) {
|
switch (self.flatTypeAuthorCount(name, from)) {
|
||||||
.none => {},
|
.none => {},
|
||||||
.one => |tid| return .{ .resolved = tid },
|
.one => |tid| return .{ .resolved = tid },
|
||||||
@@ -13993,24 +14028,59 @@ pub const Lowering = struct {
|
|||||||
return .not_visible;
|
return .not_visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single-hop non-transitive visibility gate for an UNQUALIFIED type-returning
|
/// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED
|
||||||
/// FUNCTION head used as a type (`Make(N, T)` where `Make :: ($K, $T) -> Type`).
|
/// type-returning FUNCTION head used as a type (`Make(N, T)` where
|
||||||
/// A type-fn is a `fn_decl`, so its visibility is FUNCTION visibility
|
/// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so its visibility is
|
||||||
/// (`isNameVisible`, the single-hop flat name set) — NOT the type-author model.
|
/// FUNCTION visibility (`isNameVisible`, the single-hop flat name set) — NOT the
|
||||||
/// Emits + returns TRUE when the fn is authored somewhere but not reachable
|
/// type-author model. Returns TRUE (loud diagnostic already emitted) when the
|
||||||
/// from the use site (a 2-flat-hop leak). A scope-local (mangled) type-fn is
|
/// head is AMBIGUOUS (≥2 distinct direct flat same-name type-fn authors, no own
|
||||||
/// visible in its own scope and exempt; falls open when unwired / default-
|
/// author — consistent with the parameterized struct / protocol heads and the
|
||||||
/// context. Diagnostic mirrors the type form (the head IS used as a type here).
|
/// 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 {
|
fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
|
||||||
if (self.emitting_default_context) return false;
|
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;
|
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.isNameVisible(name)) return false;
|
||||||
if (self.diagnostics) |d|
|
if (self.diagnostics) |d|
|
||||||
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
||||||
return true;
|
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)).
|
/// 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 {
|
fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
|
||||||
// A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is
|
// A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is
|
||||||
|
|||||||
Reference in New Issue
Block a user