fix(stdlib/E4): collapse generic-struct author matrix into four choke-points
The generic-struct author-selection matrix {bare,qualified} × {site} × {layout,
body} drifted per-site across 12 attempts because method bodies were resolved by
bare template name in `fn_ast_map["Box.method"]`, independent of which author
produced the instance's layout. Collapse it into four choke-points so
layout-author ≡ body-author by construction:
CP-1 `selectGenericStructHead` — the single layout-head selector every generic
struct head site funnels through (alias-RHS .call/.parameterized, array-
literal, static head, resolveTypeCall/ParameterizedWithBindings). Emits the
visibility / missing-member diagnostics inline; returns a control-flow-only
union. No head site reads `struct_template_map` for selection directly.
CP-2 author stamp — non-optional `decl: *StructDecl` on `StructTemplate` (set at
the sole producer `buildGenericStructTemplate`) + `struct_instance_author`
written at `instantiateGenericStruct` from the SAME `tmpl` that builds the
layout; re-stamped on the dedup fast-path so an instance is never returned
without an author.
CP-3 alias metadata copy — mirror template/bindings/author from the mangled
instance onto the alias display name, so an `ABox`-typed receiver is a
first-class dispatch instance (Counter-2).
CP-4 `genericInstanceMethod` / `ensureGenericInstanceMethodLowered` — the single
body reader: inline methods select via the stamped author (`structMethodFn`,
source-pin follows for free); impl-block methods fall back to the template-
keyed `fn_ast_map` entry. Routes the four bespoke body sites (static head,
instance dispatch, param typing, protocol thunk) + the new qualified static
head (`a.Box(s64).make(7)`, finding #2).
A debug assert locks `struct_instance_author` / `struct_instance_template` keyset
coincidence so a future third writer that forgets the author trips a test.
Goldens 0777/0778/0780 (bare instance method — ptr/by-value/param-typed, finding
#1), 0779/0785 (qualified static head + missing member, finding #2), 0783 (alias
instance dispatch, Counter-2), 0782 (ambiguity containment). 0414/0415/0543 and
the FFI suites stay green.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// A BARE generic struct INSTANCE method (`x.tag()`) must dispatch to the method
|
||||
// authored alongside the instance's layout — the single bare-VISIBLE author —
|
||||
// NOT the global last-wins `fn_ast_map["Box.tag"]`, which a NON-visible
|
||||
// 2-flat-hop same-name template's method can win.
|
||||
//
|
||||
// `b.sx` declares a one-field `Box($T)` (size 8) whose `tag` returns `self.x+8`,
|
||||
// and itself flat-imports `c.sx`, whose two-field `Box($T)` (size 16) declares a
|
||||
// `tag` returning `self.x+16`. This file flat-imports ONLY `b.sx`, so `b.Box` is
|
||||
// one flat hop away (visible) and `c.Box` is two hops (NOT bare-visible).
|
||||
//
|
||||
// Regression (Phase E4 finding #1, instance-method site): the static head
|
||||
// `Box(s64).make(7)` already selected `b.Box` for the layout (size 8), but the
|
||||
// instance method `x.tag()` resolved by bare template name in `fn_ast_map`, so it
|
||||
// ran `c.Box.tag` (returning 7+16=23) on the b instance. Fail-before printed
|
||||
// `size=8 tag=23`; with body-author ≡ layout-author it runs b's `tag` (7+8=15).
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0777-modules-bare-generic-instance-method-visible-author/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
x := Box(s64).make(7);
|
||||
print("size={} tag={}\n", size_of(type_of(x)), x.tag());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// The bare-VISIBLE author: a one-field generic `Box` (size 8). Its `tag` returns
|
||||
// `self.x + 8`. `b.sx` itself flat-imports `c.sx`, so a file importing only b.sx
|
||||
// reaches `c.Box` (and `c.Box.tag`) at two hops and must NOT dispatch to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value }
|
||||
}
|
||||
|
||||
tag :: (self: *Box) -> T {
|
||||
self.x + 8
|
||||
}
|
||||
}
|
||||
|
||||
#import "c.sx";
|
||||
@@ -0,0 +1,16 @@
|
||||
// The NON-visible 2-flat-hop author: a two-field generic `Box` (size 16) whose
|
||||
// `tag` returns `self.x + 16`. Same template NAME as b's, different body. It wins
|
||||
// the global last-wins `fn_ast_map["Box.tag"]`, so a file importing only b.sx
|
||||
// must NOT dispatch `x.tag()` to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
y: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value, y = value }
|
||||
}
|
||||
|
||||
tag :: (self: *Box) -> T {
|
||||
self.x + 16
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// A BARE generic struct instance method with a BY-VALUE receiver (`self: Box`)
|
||||
// must dispatch to the VISIBLE author's method, whose receiver ABI matches the
|
||||
// VISIBLE author's layout. The single visible `b.Box` is one field (size 8); the
|
||||
// 2-flat-hop `c.Box` is two fields (size 16). Selecting `c.Box.dbl` (a `{i64,i64}`
|
||||
// by-value receiver) for a `b.Box` instance (`{i64}`) mismatches the layout.
|
||||
//
|
||||
// Regression (Phase E4 finding #1, by-value receiver variant): before
|
||||
// body-author ≡ layout-author, the instance method resolved by bare template name
|
||||
// and selected `c.Box.dbl`, whose by-value receiver signature did not match the
|
||||
// `b.Box` instance layout (an LLVM verify failure / wrong body). With the fix it
|
||||
// runs `b.Box.dbl` (`self.x * 2`).
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0778-modules-bare-generic-instance-by-value-receiver/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
x := Box(s64).make(5);
|
||||
print("size={} dbl={}\n", size_of(type_of(x)), x.dbl());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// The bare-VISIBLE author: a one-field generic `Box` (size 8) with a BY-VALUE
|
||||
// receiver method `dbl` (`self: Box`). `b.sx` flat-imports `c.sx`, so a file
|
||||
// importing only b.sx reaches `c.Box` at two hops and must NOT dispatch to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value }
|
||||
}
|
||||
|
||||
dbl :: (self: Box) -> T {
|
||||
self.x * 2
|
||||
}
|
||||
}
|
||||
|
||||
#import "c.sx";
|
||||
@@ -0,0 +1,17 @@
|
||||
// The NON-visible 2-flat-hop author: a two-field generic `Box` (size 16) with a
|
||||
// BY-VALUE receiver `dbl` (`self: Box`) returning `self.x * 2 + 100`. Its
|
||||
// by-value receiver ABI (`{i64,i64}`) differs from b's (`{i64}`); it wins the
|
||||
// global last-wins `fn_ast_map["Box.dbl"]`, so a file importing only b.sx must
|
||||
// NOT dispatch `x.dbl()` to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
y: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value, y = value }
|
||||
}
|
||||
|
||||
dbl :: (self: Box) -> T {
|
||||
self.x * 2 + 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// A QUALIFIED generic static-method head (`a.Box(s64).make(7)`) must instantiate
|
||||
// — and call the `make` of — the template AUTHORED by namespace `a`, for BOTH
|
||||
// the type layout and the method body. Two namespaces each author a same-name
|
||||
// generic `Box($T)` with a DIFFERENT layout and a DIFFERENT `make` (a: one field;
|
||||
// b: two fields, sets `y = v+100`). `a.Box(s64).make(7)` and `b.Box(s64).make(9)`
|
||||
// must select their OWN module's author.
|
||||
//
|
||||
// Regression (Phase E4 finding #2, qualified static-method site): the static-head
|
||||
// path only handled an IDENTIFIER inner callee (`Box(..)`), so the qualified
|
||||
// inner callee `a.Box(..)` (a `.field_access`) was not routed through
|
||||
// `qualifiedStructTemplate` — `a.Box(s64).make(7)` resolved to nothing
|
||||
// (unresolved, exit 1).
|
||||
|
||||
#import "modules/std.sx";
|
||||
a :: #import "0779-modules-qualified-generic-static-method-author/a.sx";
|
||||
b :: #import "0779-modules-qualified-generic-static-method-author/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
xa := a.Box(s64).make(7);
|
||||
xb := b.Box(s64).make(9);
|
||||
print("a.x={} b.x={} b.y={} sizes={} {}\n", xa.x, xb.x, xb.y, size_of(type_of(xa)), size_of(type_of(xb)));
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Author A's generic `Box` — one s64 field (size 8). Its `make` sets only `x`.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Author B's generic `Box` — two s64 fields (size 16). Same template NAME as
|
||||
// A's, different layout AND different `make` (sets `y = value + 100`). The
|
||||
// qualified static head must select by namespace author for both.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
y: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value, y = value + 100 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// A BARE generic struct instance method that takes a PARAM (`x.combine(3)`) must
|
||||
// type its arguments against — and run the body of — the VISIBLE author's method.
|
||||
// The arg-typing path (`resolveCallParamTypes`) reads the instance's STAMPED
|
||||
// author for the method's param types, and dispatch runs that same author's body.
|
||||
//
|
||||
// `b.sx` (one field, size 8) declares `combine(self, v) => self.x + v`; the
|
||||
// 2-flat-hop `c.sx` (two fields) declares `combine(self, v) => self.x * v + 16`.
|
||||
// Importing only b.sx, `x.combine(3)` on the visible `b.Box` instance must run
|
||||
// b's body (`7 + 3 = 10`), not c's (`7 * 3 + 16 = 37`).
|
||||
//
|
||||
// Regression (Phase E4 finding #1, param-typed instance-method site).
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0780-modules-bare-generic-instance-param-typed-author/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
x := Box(s64).make(7);
|
||||
print("combine={}\n", x.combine(3));
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// The bare-VISIBLE author: a one-field generic `Box` (size 8). `combine` adds the
|
||||
// param. `b.sx` flat-imports `c.sx`, so a file importing only b.sx reaches
|
||||
// `c.Box` (and `c.Box.combine`) at two hops and must NOT dispatch to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value }
|
||||
}
|
||||
|
||||
combine :: (self: *Box, v: T) -> T {
|
||||
self.x + v
|
||||
}
|
||||
}
|
||||
|
||||
#import "c.sx";
|
||||
@@ -0,0 +1,16 @@
|
||||
// The NON-visible 2-flat-hop author: a two-field generic `Box` (size 16) whose
|
||||
// `combine` returns `self.x * v + 16`. Same template NAME / method name as b's,
|
||||
// different body. It wins the global last-wins `fn_ast_map["Box.combine"]`, so a
|
||||
// file importing only b.sx must NOT dispatch `x.combine(..)` to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
y: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value, y = value }
|
||||
}
|
||||
|
||||
combine :: (self: *Box, v: T) -> T {
|
||||
self.x * v + 16
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// A BARE generic static-method head `Box(s64).make(7)` whose name has ≥2 DISTINCT
|
||||
// directly-visible (1-hop flat) same-name authors is AMBIGUOUS — the head must
|
||||
// diagnose the ambiguity (consistent with the leaf / 0755 / 0767) BEFORE any
|
||||
// instantiation or method lookup, never silently pick a global last-wins author.
|
||||
//
|
||||
// `p.sx` and `q.sx` each author a generic `Box($T)` and are BOTH flat-imported
|
||||
// here, so a bare `Box` reference has two visible authors and cannot be resolved
|
||||
// without qualification. The fix is to `p.Box(s64)` / `q.Box(s64)` (0779).
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0782-modules-bare-generic-instance-ambiguous-authors/p.sx";
|
||||
#import "0782-modules-bare-generic-instance-ambiguous-authors/q.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
x := Box(s64).make(7);
|
||||
print("{}\n", x.x);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Author P's generic `Box` — one s64 field. Flat-imported alongside q.sx's
|
||||
// same-name `Box`, so a bare `Box` reference is ambiguous.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Author Q's generic `Box` — two s64 fields. Flat-imported alongside p.sx's
|
||||
// same-name `Box`, so a bare `Box` reference is ambiguous.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
y: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value, y = value }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// A generic-struct ALIAS whose RHS is a qualified head (`ABox :: a.Box(s64)`)
|
||||
// must make an ALIAS-typed receiver (`x: ABox`) a first-class dispatch instance:
|
||||
// `x.tag()` runs the author `a`'s body with `a`'s bindings — never a dead end.
|
||||
// Two namespaces author a same-name generic `Box($T)` with a DIFFERENT layout and
|
||||
// a DIFFERENT `tag`; the alias over `a.Box(s64)` must dispatch `a.Box.tag`.
|
||||
//
|
||||
// Regression (Phase E4 Counter-2): the alias registration cloned the layout into
|
||||
// a fresh type named `ABox` but did NOT mirror the instance template/bindings/
|
||||
// author onto the alias name, so `x.tag()` on an `ABox` receiver resolved to
|
||||
// nothing (unresolved). Copying the three instance-map entries to the alias name
|
||||
// makes the alias receiver dispatch its author's method.
|
||||
|
||||
#import "modules/std.sx";
|
||||
a :: #import "0783-modules-qualified-generic-alias-instance-dispatch/a.sx";
|
||||
b :: #import "0783-modules-qualified-generic-alias-instance-dispatch/b.sx";
|
||||
|
||||
ABox :: a.Box(s64);
|
||||
|
||||
main :: () -> s32 {
|
||||
x : ABox = .{ x = 5 };
|
||||
print("size={} tag={}\n", size_of(ABox), x.tag());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Author A's generic `Box` — one s64 field (size 8). Its `tag` returns
|
||||
// `self.x + 1`. The alias `ABox :: a.Box(s64)` must dispatch to THIS `tag`.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
tag :: (self: *Box) -> T {
|
||||
self.x + 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Author B's generic `Box` — two s64 fields (size 16) whose `tag` returns
|
||||
// `self.x + 2`. Same template NAME as A's, different layout/body; it wins the
|
||||
// global last-wins `fn_ast_map["Box.tag"]`, so the alias over `a.Box` must NOT
|
||||
// dispatch to it.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
y: T;
|
||||
|
||||
tag :: (self: *Box) -> T {
|
||||
self.x + 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// A QUALIFIED generic static-method head `a.Box(s64).make(7)` where namespace `a`
|
||||
// exists but authors NO member named `Box` must DIAGNOSE the missing member —
|
||||
// never silently fall back to the bare last-wins `struct_template_map` and
|
||||
// instantiate an unrelated module's same-name `Box` (parallels 0775 for the
|
||||
// static-method head).
|
||||
//
|
||||
// `a.sx` authors only `Other` (no `Box`); `b.sx` authors a generic `Box($T)`.
|
||||
// The qualified static head `a.Box(s64).make(7)` must report that `a` has no
|
||||
// member `Box`, NOT resolve to `b.Box.make`.
|
||||
|
||||
#import "modules/std.sx";
|
||||
a :: #import "0785-modules-qualified-generic-static-missing-member/a.sx";
|
||||
b :: #import "0785-modules-qualified-generic-static-missing-member/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
x := a.Box(s64).make(7);
|
||||
print("{}\n", x.x);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Namespace A authors only `Other` — NO `Box`. The qualified static head
|
||||
// `a.Box(s64).make(..)` must diagnose the missing member, not fall to b's `Box`.
|
||||
Other :: struct($T: Type) {
|
||||
v: T;
|
||||
|
||||
make :: (value: T) -> Other(T) {
|
||||
.{ v = value }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Namespace B authors a generic `Box($T)` with `make`. It wins the global
|
||||
// last-wins `struct_template_map`, so the qualified head `a.Box(..)` must NOT
|
||||
// silently resolve to this when namespace `a` lacks `Box`.
|
||||
Box :: struct($T: Type) {
|
||||
x: T;
|
||||
|
||||
make :: (value: T) -> Box(T) {
|
||||
.{ x = value }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
size=8 tag=15
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
size=8 dbl=10
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
a.x=7 b.x=9 b.y=109 sizes=8 16
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
combine=10
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: type 'Box' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import
|
||||
--> examples/0782-modules-bare-generic-instance-ambiguous-authors.sx:15:10
|
||||
|
|
||||
15 | x := Box(s64).make(7);
|
||||
| ^^^
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
size=8 tag=6
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: namespace 'a' has no member 'Box'
|
||||
--> examples/0785-modules-qualified-generic-static-missing-member.sx:16:10
|
||||
|
|
||||
16 | x := a.Box(s64).make(7);
|
||||
| ^^^^^
|
||||
547
src/ir/lower.zig
547
src/ir/lower.zig
@@ -233,6 +233,7 @@ pub const Lowering = struct {
|
||||
struct_defaults_map: std.StringHashMap([]const ?*const Node) = std.StringHashMap([]const ?*const Node).init(std.heap.page_allocator), // struct name → field defaults
|
||||
struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)) = std.StringHashMap(std.StringHashMap(TypeId)).init(std.heap.page_allocator), // mangled struct name → type param bindings
|
||||
struct_instance_template: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // mangled struct name → template name
|
||||
struct_instance_author: std.StringHashMap(*const ast.StructDecl) = std.StringHashMap(*const ast.StructDecl).init(std.heap.page_allocator), // mangled struct name → authoring StructDecl (CP-2: body-author ≡ layout-author)
|
||||
comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value)
|
||||
protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds
|
||||
protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId
|
||||
@@ -524,6 +525,9 @@ pub const Lowering = struct {
|
||||
// then the user-declared params (with type-erased pointers since JNI
|
||||
// doesn't carry sx-side types across the binding).
|
||||
self.synthesizeJniMainStubs();
|
||||
// CP coverage lock: every generic instance carries both a template and an
|
||||
// author stamp (body-author ≡ layout-author by construction).
|
||||
self.assertInstanceMapsCoincide();
|
||||
}
|
||||
|
||||
/// ERR E4.2: the entry-point signature gate. `main` must take no parameters
|
||||
@@ -1030,55 +1034,38 @@ pub const Lowering = struct {
|
||||
call_data.callee.data.field_access.object.data.identifier.name
|
||||
else
|
||||
null;
|
||||
const qual_call_tmpl: ?StructTemplate = if (qual_alias) |a| self.qualifiedStructTemplate(a, callee_name) else null;
|
||||
if (callee_name.len > 0) {
|
||||
if (qual_call_tmpl) |qt| {
|
||||
self.registerGenericStructAlias(cd.name, &qt, call_data.args);
|
||||
} else if (qual_alias != null and self.qualifiedMemberMissing(qual_alias.?, callee_name)) {
|
||||
// Qualified alias head whose namespace lacks the member:
|
||||
// diagnose, never fall to the bare global template (E4 #2).
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, call_data.callee.span, "namespace '{s}' has no member '{s}'", .{ qual_alias.?, callee_name });
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
} else if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| reg: {
|
||||
// 2-hop generic-struct head leak: poison the alias with
|
||||
// `.unresolved` (suppressed downstream) so the use site
|
||||
// sees no fabricated-stub cascade, only the loud
|
||||
// "not visible" diagnostic the gate already emitted.
|
||||
if (!head_qualified and self.headTypeLeak(callee_name, call_data.callee.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
break :reg;
|
||||
}
|
||||
// A bare alias head selects the single bare-VISIBLE
|
||||
// author's template, not the global last-wins map (E4 #1).
|
||||
if (!head_qualified) {
|
||||
if (self.bareVisibleStructTemplate(callee_name)) |vt| {
|
||||
self.registerGenericStructAlias(cd.name, &vt, call_data.args);
|
||||
break :reg;
|
||||
// Generic-struct alias head (`ABox :: Box(s64)` /
|
||||
// `a.Box(s64)`): route layout selection through the single
|
||||
// choke-point (CP-1); the Vector / type-fn branches stay
|
||||
// as the non-generic fall-through.
|
||||
switch (self.selectGenericStructHead(callee_name, qual_alias, head_qualified, call_data.callee.span)) {
|
||||
.template => |t| self.registerGenericStructAlias(cd.name, &t, call_data.args),
|
||||
.poisoned => self.putTypeAlias(self.current_source_file, cd.name, .unresolved),
|
||||
.not_generic => {
|
||||
if (std.mem.eql(u8, callee_name, "Vector")) {
|
||||
// Builtin type constructor — checked BEFORE
|
||||
// the generic `fn_ast_map` branch because
|
||||
// `Vector` IS in `fn_ast_map` (declared as a
|
||||
// `#builtin` fn) but `instantiateTypeFunction`
|
||||
// can't resolve it (no body). Use
|
||||
// `resolveTypeCallWithBindings` which
|
||||
// hard-codes the vector layout.
|
||||
const result_ty = self.resolveTypeCallWithBindings(call_data);
|
||||
if (result_ty != .void) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
} else if (self.program_index.fn_ast_map.get(callee_name)) |fd| {
|
||||
// Type-returning function: Foo :: Complex(u32)
|
||||
if (fd.type_params.len > 0) {
|
||||
if (!head_qualified and self.headFnLeak(callee_name, call_data.callee.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
} else if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.registerGenericStructAlias(cd.name, tmpl, call_data.args);
|
||||
} else if (std.mem.eql(u8, callee_name, "Vector")) {
|
||||
// Builtin type constructor — checked BEFORE
|
||||
// the generic `fn_ast_map` branch because
|
||||
// `Vector` IS in `fn_ast_map` (declared as a
|
||||
// `#builtin` fn) but `instantiateTypeFunction`
|
||||
// can't resolve it (no body). Use
|
||||
// `resolveTypeCallWithBindings` which
|
||||
// hard-codes the vector layout.
|
||||
const result_ty = self.resolveTypeCallWithBindings(call_data);
|
||||
if (result_ty != .void) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
} else if (self.program_index.fn_ast_map.get(callee_name)) |fd| {
|
||||
// Type-returning function: Foo :: Complex(u32)
|
||||
if (fd.type_params.len > 0) {
|
||||
if (!head_qualified and self.headFnLeak(callee_name, call_data.callee.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
} else if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
} else if (cd.value.data == .parameterized_type_expr) {
|
||||
@@ -1091,38 +1078,22 @@ pub const Lowering = struct {
|
||||
// head site `resolveParameterizedWithBindings`), not the
|
||||
// bare last-wins `struct_template_map`.
|
||||
const pt_alias: ?[]const u8 = if (pt_qualified) pt.name[0 .. std.mem.indexOfScalar(u8, pt.name, '.').?] else null;
|
||||
const qual_pt_tmpl: ?StructTemplate = if (pt_alias) |a| self.qualifiedStructTemplate(a, base_name) else null;
|
||||
if (qual_pt_tmpl) |qt| {
|
||||
self.registerGenericStructAlias(cd.name, &qt, pt.args);
|
||||
} else if (pt_alias != null and self.qualifiedMemberMissing(pt_alias.?, base_name)) {
|
||||
// Qualified alias base whose namespace lacks the member:
|
||||
// diagnose, never fall to the bare global template (E4 #2).
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, cd.value.span, "namespace '{s}' has no member '{s}'", .{ pt_alias.?, base_name });
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
} else if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| reg: {
|
||||
if (!pt_qualified and self.headTypeLeak(base_name, cd.value.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
break :reg;
|
||||
}
|
||||
// A bare alias base selects the single bare-VISIBLE author's
|
||||
// template, not the global last-wins map (E4 #1).
|
||||
if (!pt_qualified) {
|
||||
if (self.bareVisibleStructTemplate(base_name)) |vt| {
|
||||
self.registerGenericStructAlias(cd.name, &vt, pt.args);
|
||||
break :reg;
|
||||
// Generic-struct alias base: route layout selection through the
|
||||
// single choke-point (CP-1); the builtin parameterised-type
|
||||
// path (Vector etc.) stays as the non-generic fall-through.
|
||||
switch (self.selectGenericStructHead(base_name, pt_alias, pt_qualified, cd.value.span)) {
|
||||
.template => |t| self.registerGenericStructAlias(cd.name, &t, pt.args),
|
||||
.poisoned => self.putTypeAlias(self.current_source_file, cd.name, .unresolved),
|
||||
.not_generic => {
|
||||
// Builtin parameterised type (Vector(N, T) etc) —
|
||||
// resolve via type_bridge and register the result
|
||||
// under the alias name so `Vec4` in expression
|
||||
// position can `const_type(<vector tid>)`.
|
||||
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
if (result_ty != .void and result_ty != .unresolved) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
}
|
||||
self.registerGenericStructAlias(cd.name, tmpl, pt.args);
|
||||
} else {
|
||||
// Builtin parameterised type (Vector(N, T) etc) —
|
||||
// resolve via type_bridge and register the result
|
||||
// under the alias name so `Vec4` in expression
|
||||
// position can `const_type(<vector tid>)`.
|
||||
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
if (result_ty != .void and result_ty != .unresolved) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// comptime_expr handled in Pass 2
|
||||
@@ -6966,17 +6937,17 @@ pub const Lowering = struct {
|
||||
return self.module.types.vectorOf(elem, length);
|
||||
}
|
||||
}
|
||||
// Try as generic struct
|
||||
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
||||
const bare = cl.callee.data != .field_access;
|
||||
if (bare and self.headTypeLeak(callee_name, cl.callee.span)) return .unresolved;
|
||||
// A bare head selects the single bare-VISIBLE author, not the
|
||||
// global last-wins map (E4 #1).
|
||||
if (bare) {
|
||||
if (self.bareVisibleStructTemplate(callee_name)) |vt|
|
||||
return self.instantiateGenericStruct(&vt, cl.args);
|
||||
// Generic-struct typed-literal head (`Box(s64).[...]`): route
|
||||
// through the single layout choke-point (CP-1). A qualified head
|
||||
// `a.Box(s64).[...]` selects a's OWN template via the namespace edge
|
||||
// (Counter-1: was the global last-wins map); a bare head selects the
|
||||
// single bare-VISIBLE author.
|
||||
if (headNameOfCallee(cl.callee)) |hn| {
|
||||
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) {
|
||||
.template => |t| return self.instantiateGenericStruct(&t, cl.args),
|
||||
.poisoned => return .unresolved,
|
||||
.not_generic => {},
|
||||
}
|
||||
return self.instantiateGenericStruct(tmpl, cl.args);
|
||||
}
|
||||
return .unresolved;
|
||||
},
|
||||
@@ -8724,54 +8695,34 @@ pub const Lowering = struct {
|
||||
// Type constructor call: Sx(f32).user(0.5) — obj is a call that returns a type
|
||||
if (fa.object.data == .call) {
|
||||
const inner_call = &fa.object.data.call;
|
||||
// Generic struct STATIC-METHOD head (`Box(s64).make(..)` or the
|
||||
// qualified `a.Box(s64).make(..)`): the layout author is chosen
|
||||
// by the single head choke-point (CP-1) and the method body by
|
||||
// the instance's STAMPED author (CP-4), so layout-author ≡
|
||||
// body-author for BOTH bare and qualified heads (E4 #1 / #2).
|
||||
if (headNameOfCallee(inner_call.callee)) |hn| {
|
||||
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, inner_call.callee.span)) {
|
||||
.poisoned => return Ref.none,
|
||||
.template => |t| {
|
||||
const inst_ty = self.instantiateGenericStruct(&t, inner_call.args);
|
||||
const inst_name = self.formatTypeName(inst_ty);
|
||||
if (self.genericInstanceMethod(inst_name, fa.field)) |gm| {
|
||||
if (self.ensureGenericInstanceMethodLowered(gm)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const final_args = self.prependCtxIfNeeded(func, args.items);
|
||||
self.coerceCallArgs(final_args, func.params);
|
||||
return self.builder.call(fid, final_args, func.ret);
|
||||
}
|
||||
}
|
||||
},
|
||||
.not_generic => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (inner_call.callee.data == .identifier) {
|
||||
const inner_name = inner_call.callee.data.identifier.name;
|
||||
const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name;
|
||||
|
||||
// Generic struct static method: Animated(Size).make(...).
|
||||
// `inner_call.callee` is an identifier here (guarded above), so
|
||||
// the head is unqualified and subject to the bare-head gate (E4).
|
||||
if (self.program_index.struct_template_map.getPtr(resolved)) |tmpl| {
|
||||
if (self.headTypeLeak(inner_name, inner_call.callee.span)) return Ref.none;
|
||||
// A bare generic static-method head (`Box(s64).make(..)`)
|
||||
// selects the single bare-VISIBLE author for BOTH the
|
||||
// instantiated type layout AND the method body — never the
|
||||
// global last-wins map, which a non-visible 2-flat-hop
|
||||
// same-name template (and its name-keyed `Box.make`) can win
|
||||
// (E4 #1, static-method site).
|
||||
const vis = self.bareVisibleStructDecl(inner_name);
|
||||
var vt: StructTemplate = undefined;
|
||||
const use_tmpl: *const StructTemplate = if (vis) |v| blk: {
|
||||
vt = self.buildGenericStructTemplate(v.sd, v.source) orelse break :blk tmpl;
|
||||
break :blk &vt;
|
||||
} else tmpl;
|
||||
const inst_ty = self.instantiateGenericStruct(use_tmpl, inner_call.args);
|
||||
const inst_name = self.formatTypeName(inst_ty);
|
||||
// Look up template method, monomorphize, and call
|
||||
if (self.struct_instance_template.get(inst_name)) |tmpl_name| {
|
||||
const method_fd: ?*const ast.FnDecl = if (vis) |v|
|
||||
structMethodFn(v.sd, fa.field)
|
||||
else fm: {
|
||||
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field;
|
||||
break :fm self.program_index.fn_ast_map.get(tmpl_qualified);
|
||||
};
|
||||
if (method_fd) |fd| {
|
||||
if (self.struct_instance_bindings.getPtr(inst_name)) |bindings| {
|
||||
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ inst_name, fa.field }) catch fa.field;
|
||||
if (!self.lowered_functions.contains(mangled)) {
|
||||
self.monomorphizeFunction(fd, mangled, bindings);
|
||||
}
|
||||
if (self.resolveFuncByName(mangled)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const final_args = self.prependCtxIfNeeded(func, args.items);
|
||||
self.coerceCallArgs(final_args, func.params);
|
||||
return self.builder.call(fid, final_args, func.ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.program_index.fn_ast_map.get(resolved)) |fd| {
|
||||
if (fd.type_params.len > 0) {
|
||||
if (self.headFnLeak(inner_name, inner_call.callee.span)) return Ref.none;
|
||||
@@ -8981,29 +8932,20 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for generic struct template method
|
||||
if (self.struct_instance_template.get(sname)) |tmpl_name| {
|
||||
// This is an instantiated generic struct — look up template method
|
||||
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field;
|
||||
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
||||
// Get the stored type bindings for this instance
|
||||
if (self.struct_instance_bindings.getPtr(sname)) |bindings| {
|
||||
// Monomorphize the method with the struct's type bindings
|
||||
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field;
|
||||
if (!self.lowered_functions.contains(mangled)) {
|
||||
self.monomorphizeFunction(fd, mangled, bindings);
|
||||
}
|
||||
if (self.resolveFuncByName(mangled)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const ret_ty = func.ret;
|
||||
const params = func.params;
|
||||
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
||||
self.appendDefaultArgs(fd, &method_args);
|
||||
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
||||
self.coerceCallArgs(final_args, params);
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
}
|
||||
}
|
||||
// Generic-struct instance method: select the body via the
|
||||
// instance's STAMPED author (CP-4), so the dispatched method is
|
||||
// the one authored alongside this instance's layout — never the
|
||||
// global last-wins `fn_ast_map["Template.method"]`.
|
||||
if (self.genericInstanceMethod(sname, fa.field)) |gm| {
|
||||
if (self.ensureGenericInstanceMethodLowered(gm)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const ret_ty = func.ret;
|
||||
const params = func.params;
|
||||
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
||||
self.appendDefaultArgs(gm.fd, &method_args);
|
||||
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
||||
self.coerceCallArgs(final_args, params);
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13200,24 +13142,20 @@ pub const Lowering = struct {
|
||||
return types_list.items;
|
||||
}
|
||||
}
|
||||
// Try generic struct template method: List__Container.append → List.append
|
||||
// with type bindings from the struct instantiation
|
||||
if (self.struct_instance_template.get(sname)) |tmpl_name| {
|
||||
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch return &.{};
|
||||
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
||||
if (fd.params.len > 0) {
|
||||
// Temporarily set type_bindings so resolveParamType can substitute T → concrete type
|
||||
const saved_bindings = self.type_bindings;
|
||||
if (self.struct_instance_bindings.getPtr(sname)) |bindings| {
|
||||
self.type_bindings = bindings.*;
|
||||
}
|
||||
var types_list = std.ArrayList(TypeId).empty;
|
||||
for (fd.params[1..]) |p| {
|
||||
types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable;
|
||||
}
|
||||
self.type_bindings = saved_bindings;
|
||||
return types_list.items;
|
||||
// Generic-struct instance method param types: select the method
|
||||
// body via the instance's STAMPED author (CP-4), substituting the
|
||||
// instance's bindings so `T → concrete`. The param source-pin
|
||||
// follows the selected `fd` (its own `body.source_file`).
|
||||
if (self.genericInstanceMethod(sname, fa.field)) |gm| {
|
||||
if (gm.fd.params.len > 0) {
|
||||
const saved_bindings = self.type_bindings;
|
||||
self.type_bindings = gm.bindings.*;
|
||||
var types_list = std.ArrayList(TypeId).empty;
|
||||
for (gm.fd.params[1..]) |p| {
|
||||
types_list.append(self.alloc, self.resolveParamTypeInSource(gm.fd.body.source_file, &p)) catch unreachable;
|
||||
}
|
||||
self.type_bindings = saved_bindings;
|
||||
return types_list.items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14044,6 +13982,77 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Control-flow outcome of the generic-struct LAYOUT-head selector. Carries no
|
||||
/// diagnostic for the caller to emit — `selectGenericStructHead` emits inline.
|
||||
const HeadTemplate = union(enum) {
|
||||
template: StructTemplate, // visible bare author OR qualified author → instantiate
|
||||
poisoned, // gate already diagnosed → caller returns .unresolved / Ref.none
|
||||
not_generic, // name is not a generic struct head → caller's non-struct path
|
||||
};
|
||||
|
||||
/// THE single selector every generic-struct LAYOUT-head site funnels through —
|
||||
/// no head site reads `struct_template_map` for selection directly. Decides the
|
||||
/// authoring template for a head named `name`, qualified by namespace `alias`
|
||||
/// (non-null only for `ns.Box(..)` with an identifier object) and flagged
|
||||
/// `is_qualified` (any `.field_access` callee, including a non-identifier
|
||||
/// object). Emits the visibility / missing-member diagnostics INLINE at `span`,
|
||||
/// at the same program point and ordering the sites used before (0767/0769/0775),
|
||||
/// and returns a control-flow-only outcome:
|
||||
/// - qualified, namespace authors `name` as a generic struct → that author.
|
||||
/// - qualified, namespace exists but lacks `name` → diagnose missing member,
|
||||
/// `.poisoned` (never the bare global map, E4 #2).
|
||||
/// - qualified, namespace authors `name` but NOT as a generic struct (a
|
||||
/// type-fn / named type) → `.not_generic` (caller's non-struct path).
|
||||
/// - qualified with no usable alias (nested-ns object) → the global template
|
||||
/// if one exists (pre-existing behavior; no namespace edge to consult).
|
||||
/// - bare, ≥2 visible authors / 2-flat-hop only → `headTypeLeak` diagnosed →
|
||||
/// `.poisoned`.
|
||||
/// - bare, single visible author → that author (own / 1-hop flat), source-keyed.
|
||||
/// - bare, visible author IS the canonical map author → the global template
|
||||
/// (byte-identical single-author path).
|
||||
/// - not in `struct_template_map` at all → `.not_generic`.
|
||||
fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate {
|
||||
if (is_qualified) {
|
||||
if (alias) |a| {
|
||||
if (self.qualifiedStructTemplate(a, name)) |tmpl| return .{ .template = tmpl };
|
||||
if (self.qualifiedMemberMissing(a, name)) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "namespace '{s}' has no member '{s}'", .{ a, name });
|
||||
return .poisoned;
|
||||
}
|
||||
return .not_generic;
|
||||
}
|
||||
// Qualified but un-aliasable object (nested namespace / non-identifier):
|
||||
// no namespace edge to select from — use the global template if present.
|
||||
if (self.program_index.struct_template_map.getPtr(name)) |tmpl| return .{ .template = tmpl.* };
|
||||
return .not_generic;
|
||||
}
|
||||
if (self.program_index.struct_template_map.getPtr(name)) |tmpl| {
|
||||
if (self.headTypeLeak(name, span)) return .poisoned;
|
||||
if (self.bareVisibleStructTemplate(name)) |vt| return .{ .template = vt };
|
||||
return .{ .template = tmpl.* };
|
||||
}
|
||||
return .not_generic;
|
||||
}
|
||||
|
||||
/// Decompose a head callee NODE (`.identifier Box` or `.field_access ns.Box`)
|
||||
/// into the `(name, alias, is_qualified)` triple `selectGenericStructHead`
|
||||
/// consumes. `alias` is the namespace identifier only for a `.field_access`
|
||||
/// whose object is a plain identifier; a nested / non-identifier object is
|
||||
/// qualified-but-unaliased.
|
||||
const HeadName = struct { name: []const u8, alias: ?[]const u8, is_qualified: bool };
|
||||
fn headNameOfCallee(callee: *const Node) ?HeadName {
|
||||
return switch (callee.data) {
|
||||
.identifier => |id| .{ .name = id.name, .alias = null, .is_qualified = false },
|
||||
.field_access => |fa| .{
|
||||
.name = fa.field,
|
||||
.alias = if (fa.object.data == .identifier) fa.object.data.identifier.name else null,
|
||||
.is_qualified = true,
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// 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):
|
||||
@@ -14219,33 +14228,15 @@ pub const Lowering = struct {
|
||||
const elem = self.resolveTypeWithBindings(cl.args[1]);
|
||||
return self.module.types.vectorOf(elem, length);
|
||||
}
|
||||
// A qualified head `ns.Box(..)` selects ns's OWN template via the
|
||||
// namespace edge, not the bare last-wins `struct_template_map` (so the
|
||||
// ambiguity escape hatch "qualify it as ns.Box" picks the right author).
|
||||
if (is_qualified and cl.callee.data.field_access.object.data == .identifier) {
|
||||
const alias = cl.callee.data.field_access.object.data.identifier.name;
|
||||
if (self.qualifiedStructTemplate(alias, callee_name)) |tmpl| {
|
||||
return self.instantiateGenericStruct(&tmpl, cl.args);
|
||||
}
|
||||
// The namespace exists but authors no member `callee_name` — diagnose
|
||||
// the missing member; never fall back to the bare global template (E4 #2).
|
||||
if (self.qualifiedMemberMissing(alias, callee_name)) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, cl.callee.span, "namespace '{s}' has no member '{s}'", .{ alias, callee_name });
|
||||
return .unresolved;
|
||||
}
|
||||
}
|
||||
// User-defined generic struct. A BARE head selects the single bare-VISIBLE
|
||||
// author's template (own or 1-hop flat), source-keyed — NOT the global
|
||||
// last-wins map, which a non-visible 2-flat-hop same-name template can win
|
||||
// (E4 #1). A qualified head NEVER reaches the bare map (it resolved or
|
||||
// diagnosed above).
|
||||
if (!is_qualified) {
|
||||
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
||||
if (self.headTypeLeak(callee_name, cl.callee.span)) return .unresolved;
|
||||
if (self.bareVisibleStructTemplate(callee_name)) |vt|
|
||||
return self.instantiateGenericStruct(&vt, cl.args);
|
||||
return self.instantiateGenericStruct(tmpl, cl.args);
|
||||
// Generic-struct head: route through the single layout choke-point (CP-1).
|
||||
// Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed;
|
||||
// qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic);
|
||||
// never the global last-wins map for a visible-shadowed or qualified head.
|
||||
if (headNameOfCallee(cl.callee)) |hn| {
|
||||
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) {
|
||||
.template => |t| return self.instantiateGenericStruct(&t, cl.args),
|
||||
.poisoned => return .unresolved,
|
||||
.not_generic => {},
|
||||
}
|
||||
}
|
||||
// User-defined type-returning function: Complex(u32), Sx(f32)
|
||||
@@ -14286,34 +14277,16 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// A qualified base `ns.Box(..)` selects ns's OWN template via the
|
||||
// namespace edge, not the bare last-wins `struct_template_map` (so the
|
||||
// ambiguity escape hatch "qualify it as ns.Box" picks the right author).
|
||||
if (is_qualified) {
|
||||
if (std.mem.indexOfScalar(u8, pt.name, '.')) |dot| {
|
||||
const alias = pt.name[0..dot];
|
||||
if (self.qualifiedStructTemplate(alias, base_name)) |tmpl| {
|
||||
return self.instantiateGenericStruct(&tmpl, pt.args);
|
||||
}
|
||||
// Namespace exists but authors no member `base_name` — diagnose the
|
||||
// missing member; never fall back to the bare global template (E4 #2).
|
||||
if (self.qualifiedMemberMissing(alias, base_name)) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "namespace '{s}' has no member '{s}'", .{ alias, base_name });
|
||||
return .unresolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User-defined generic struct: a BARE head selects the single bare-VISIBLE
|
||||
// author's template (own or 1-hop flat), source-keyed — NOT the global
|
||||
// last-wins map (E4 #1). A qualified head NEVER reaches the bare map.
|
||||
if (!is_qualified) {
|
||||
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
|
||||
if (self.headTypeLeak(base_name, span)) return .unresolved;
|
||||
if (self.bareVisibleStructTemplate(base_name)) |vt|
|
||||
return self.instantiateGenericStruct(&vt, pt.args);
|
||||
return self.instantiateGenericStruct(tmpl, pt.args);
|
||||
// Generic-struct base: route through the single layout choke-point (CP-1).
|
||||
// Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed;
|
||||
// qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic);
|
||||
// never the global last-wins map for a visible-shadowed or qualified head.
|
||||
{
|
||||
const alias: ?[]const u8 = if (std.mem.indexOfScalar(u8, pt.name, '.')) |dot| pt.name[0..dot] else null;
|
||||
switch (self.selectGenericStructHead(base_name, alias, is_qualified, span)) {
|
||||
.template => |t| return self.instantiateGenericStruct(&t, pt.args),
|
||||
.poisoned => return .unresolved,
|
||||
.not_generic => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14353,6 +14326,79 @@ pub const Lowering = struct {
|
||||
|
||||
/// Instantiate a generic struct template with concrete args.
|
||||
/// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) }
|
||||
/// A generic-struct instance method selected via the STAMPED authoring decl:
|
||||
/// the `fn_decl` to monomorphize, the instance's stored type bindings, and the
|
||||
/// instance (mangled / alias) name the monomorphized function is keyed under.
|
||||
const GenericStructMethod = struct {
|
||||
fd: *const ast.FnDecl,
|
||||
bindings: *std.StringHashMap(TypeId),
|
||||
inst_name: []const u8,
|
||||
};
|
||||
|
||||
/// THE single body-axis reader: select `method` of generic-struct instance
|
||||
/// `inst_name` via the instance's STAMPED author (`struct_instance_author`),
|
||||
/// so body-author ≡ layout-author by construction — never the global last-wins
|
||||
/// `fn_ast_map["Template.method"]` a 2-flat-hop same-name template's method
|
||||
/// could win. Null when `inst_name` is NOT a generic instance (no author stamp)
|
||||
/// — the caller's existing non-generic `fn_ast_map` path then handles it
|
||||
/// (non-generic structs, free fns, FFI), or when the confirmed author declares
|
||||
/// no such `method` (a normal unresolved-method, handled downstream). A
|
||||
/// confirmed instance whose author is present but whose bindings are missing is
|
||||
/// a LOUD invariant failure — instantiation writes both together (CP-2).
|
||||
fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod {
|
||||
const author = self.struct_instance_author.get(inst_name) orelse return null;
|
||||
const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse
|
||||
std.debug.panic("generic struct instance '{s}' has an author but no bindings", .{inst_name});
|
||||
// INLINE struct method (`Box :: struct { make :: ... }`): selected via the
|
||||
// instance's STAMPED author, so the body is the one authored alongside the
|
||||
// layout — never the global last-wins `fn_ast_map["Template.method"]` a
|
||||
// 2-flat-hop same-name template's method could win (finding #1).
|
||||
if (structMethodFn(author, method)) |fd|
|
||||
return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name };
|
||||
// IMPL-block method (`impl P for Box { ... }`): registered under the
|
||||
// template name in `fn_ast_map`, not on the struct decl, so it is keyed by
|
||||
// template name (protocol dispatch). The author confirms this IS a generic
|
||||
// instance; the method body is the template's registered impl method.
|
||||
const tmpl_name = self.struct_instance_template.get(inst_name) orelse return null;
|
||||
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method }) catch return null;
|
||||
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd|
|
||||
return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name };
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Monomorphize (once) the selected generic-instance method under
|
||||
/// `<inst_name>.<method>` and return its FuncId. The source-pin follows the
|
||||
/// selected `fd` for free: `monomorphizeFunction` pins to `fd.body.source_file`,
|
||||
/// which is the template's defining module (the author's own method node).
|
||||
/// Null when the function fails to resolve post-monomorphization.
|
||||
fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId {
|
||||
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, m.fd.name }) catch return null;
|
||||
if (!self.lowered_functions.contains(mangled)) {
|
||||
self.monomorphizeFunction(m.fd, mangled, m.bindings);
|
||||
}
|
||||
return self.resolveFuncByName(mangled);
|
||||
}
|
||||
|
||||
/// Debug invariant (CP coverage lock): the two generic-instance maps written
|
||||
/// in lockstep at the SAME two writers (instantiation + alias copy) —
|
||||
/// `struct_instance_template` and `struct_instance_author` — must have
|
||||
/// coincident keysets. A future writer that registers an instance's layout
|
||||
/// without stamping its author (a silent body-axis reopen) trips this in a
|
||||
/// debug `zig build test`, not in production.
|
||||
fn assertInstanceMapsCoincide(self: *Lowering) void {
|
||||
if (!std.debug.runtime_safety) return;
|
||||
var it = self.struct_instance_template.keyIterator();
|
||||
while (it.next()) |k| {
|
||||
if (!self.struct_instance_author.contains(k.*))
|
||||
std.debug.panic("generic instance '{s}' has a template but no author stamp", .{k.*});
|
||||
}
|
||||
var it2 = self.struct_instance_author.keyIterator();
|
||||
while (it2.next()) |k| {
|
||||
if (!self.struct_instance_template.contains(k.*))
|
||||
std.debug.panic("generic instance '{s}' has an author but no template stamp", .{k.*});
|
||||
}
|
||||
}
|
||||
|
||||
fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId {
|
||||
const table = &self.module.types;
|
||||
|
||||
@@ -14441,6 +14487,16 @@ pub const Lowering = struct {
|
||||
// Already registered — check if it has fields
|
||||
const info = table.get(existing);
|
||||
if (info == .@"struct" and info.@"struct".fields.len > 0) {
|
||||
// A confirmed generic instance must never be returned without an
|
||||
// author stamp — the body axis (CP-4) keys method selection off
|
||||
// it. The template/bindings were written at first instantiation;
|
||||
// re-stamp the author from THIS `tmpl` if the dedup fast-path is
|
||||
// the first to reach this mangled name (e.g. a layout interned by
|
||||
// a forward reference before any method dispatch).
|
||||
if (!self.struct_instance_author.contains(mangled_name)) {
|
||||
const owned = self.alloc.dupe(u8, mangled_name) catch return existing;
|
||||
self.struct_instance_author.put(owned, tmpl.decl) catch {};
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
@@ -14496,10 +14552,14 @@ pub const Lowering = struct {
|
||||
// — otherwise `self.field` hits the 0-field generic stub.
|
||||
tb.put(tmpl.name, id) catch {};
|
||||
|
||||
// Store the type bindings and template name for method resolution
|
||||
// Store the type bindings, template name, and authoring decl for method
|
||||
// resolution. The author is stamped from the SAME `tmpl` that built the
|
||||
// layout above, so the body axis (CP-4) selects this instance's methods
|
||||
// via the layout author — never the global last-wins `fn_ast_map`.
|
||||
const owned_mangled = self.alloc.dupe(u8, mangled_name) catch return id;
|
||||
self.struct_instance_bindings.put(owned_mangled, tb) catch {};
|
||||
self.struct_instance_template.put(owned_mangled, tmpl.name) catch {};
|
||||
self.struct_instance_author.put(owned_mangled, tmpl.decl) catch {};
|
||||
|
||||
return id;
|
||||
}
|
||||
@@ -14967,6 +15027,7 @@ pub const Lowering = struct {
|
||||
.field_names = fnames,
|
||||
.field_type_nodes = ftype_nodes,
|
||||
.source_file = source_file,
|
||||
.decl = sd,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15094,6 +15155,19 @@ pub const Lowering = struct {
|
||||
// the unified writer so it lands in `type_aliases_by_source` and the
|
||||
// bare-TYPE gate treats it like any other alias.
|
||||
self.putTypeAlias(self.current_source_file, alias_name, alias_id);
|
||||
// CP-3: the alias display name (`ABox`) is the struct type name a receiver
|
||||
// typed `x: ABox` reports, so method dispatch on it looks up the instance
|
||||
// maps under `ABox`. Mirror the mangled instance's template/bindings/author
|
||||
// onto the alias name so an alias-typed receiver is a first-class dispatch
|
||||
// instance (runs the selected author's body + bindings), not a dead end.
|
||||
const inst_name = self.formatTypeName(inst_id);
|
||||
if (self.struct_instance_author.get(inst_name)) |author_decl| {
|
||||
const tmpl_name = self.struct_instance_template.get(inst_name) orelse return;
|
||||
const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse return;
|
||||
self.struct_instance_template.put(self.alloc.dupe(u8, alias_name) catch return, tmpl_name) catch {};
|
||||
self.struct_instance_bindings.put(self.alloc.dupe(u8, alias_name) catch return, bindings.*) catch {};
|
||||
self.struct_instance_author.put(self.alloc.dupe(u8, alias_name) catch return, author_decl) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void {
|
||||
@@ -15894,17 +15968,12 @@ pub const Lowering = struct {
|
||||
if (!self.lowered_functions.contains(qualified)) {
|
||||
if (self.program_index.fn_ast_map.contains(qualified)) {
|
||||
self.lazyLowerFunction(qualified);
|
||||
} else if (self.struct_instance_template.get(concrete_type_name)) |tmpl_name| {
|
||||
// Generic-struct instance (`Combined__s64_s64`): the impl method
|
||||
// is registered under the template name (`Combined.get`).
|
||||
// Monomorphize it for this instance's bindings so the thunk has a
|
||||
// concrete `Combined__s64_s64.get` to call.
|
||||
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method.name }) catch method.name;
|
||||
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
||||
if (self.struct_instance_bindings.getPtr(concrete_type_name)) |bindings| {
|
||||
self.monomorphizeFunction(fd, qualified, bindings);
|
||||
}
|
||||
}
|
||||
} else if (self.genericInstanceMethod(concrete_type_name, method.name)) |gm| {
|
||||
// Generic-struct instance (`Combined__s64_s64`): the impl method is
|
||||
// authored on the instance's STAMPED decl (CP-4). Monomorphize it
|
||||
// for this instance's bindings so the thunk has a concrete
|
||||
// `Combined__s64_s64.get` to call.
|
||||
self.monomorphizeFunction(gm.fd, qualified, gm.bindings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ test "ProgramIndex declaration maps round-trip (A1.1b)" {
|
||||
try std.testing.expect(idx.protocol_ast_map.get("Show").? == &pd);
|
||||
|
||||
// struct_template_map: generic struct name → template.
|
||||
try idx.struct_template_map.put("List", .{ .name = "List", .type_params = &.{}, .field_names = &.{}, .field_type_nodes = &.{} });
|
||||
const list_sd = ast.StructDecl{ .name = "List", .field_names = &.{}, .field_types = &.{}, .field_defaults = &.{} };
|
||||
try idx.struct_template_map.put("List", .{ .name = "List", .type_params = &.{}, .field_names = &.{}, .field_type_nodes = &.{}, .decl = &list_sd });
|
||||
try std.testing.expectEqualStrings("List", idx.struct_template_map.get("List").?.name);
|
||||
|
||||
// ufcs_alias_map: alias name → target function name.
|
||||
|
||||
@@ -15,6 +15,13 @@ pub const StructTemplate = struct {
|
||||
type_params: []const TemplateParam,
|
||||
field_names: []const []const u8,
|
||||
field_type_nodes: []const *const Node, // raw AST pointers — must be copied from heap nodes
|
||||
// The authoring `StructDecl` — the NON-optional identity that selects this
|
||||
// template's method bodies at instantiation. Stamped per-instance into
|
||||
// `struct_instance_author` from the SAME template that builds the layout, so
|
||||
// layout-author and body-author are one object and can never re-diverge (a
|
||||
// method body is resolved via this decl's own `methods`, never by the global
|
||||
// last-wins `fn_ast_map["Name.method"]`).
|
||||
decl: *const ast.StructDecl,
|
||||
// The module that DECLARED this template. Instantiation resolves the
|
||||
// field type nodes in THIS source context, not the (possibly cross-module)
|
||||
// instantiation site — so a field naming a type visible only in the
|
||||
|
||||
Reference in New Issue
Block a user