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