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:
agra
2026-06-08 20:34:53 +03:00
parent 7ba64d5756
commit 6406d0fb1f
45 changed files with 661 additions and 240 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
size=8 tag=15

View File

@@ -0,0 +1 @@
size=8 dbl=10

View File

@@ -0,0 +1 @@
a.x=7 b.x=9 b.y=109 sizes=8 16

View File

@@ -0,0 +1 @@
combine=10

View File

@@ -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);
| ^^^

View File

@@ -0,0 +1 @@
size=8 tag=6

View File

@@ -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);
| ^^^^^

View File

@@ -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);
}
}

View File

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

View File

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