fix(stdlib/E4): qualified generic head ns.Box(..) selects the namespace author

A qualified generic type head `ns.Box(args)` was stripped to its bare name and
read from the last-wins `struct_template_map`, so the namespace qualifier never
selected the template author: `a.Box(s64)` and `b.Box(s64)` (two namespaces each
authoring a same-name `Box($T)` with different layouts) both instantiated the
global same-name template. The documented ambiguity escape hatch ("qualify it as
ns.Box") silently produced the wrong layout.

Select the template via the namespace edge (importer -> alias -> NamespaceTarget)
instead of the bare map, at both the .call and parameterized-type-expr head
sites. Two same-name templates instantiated with the same args would also collide
on the mangled name `Box__s64`, so tag the non-canonical author's mangled name
with its source (the canonical bare-map author keeps the untagged name -> no
churn for single-author generics).

Extract `buildGenericStructTemplate` so the bare registration and the new
namespace-qualified selection share one template builder.

Regression: examples/0772 — two namespaces each authoring Box($T) with different
layouts; ns_a.Box(s64) and ns_b.Box(s64) resolve to their own module's template
(sizes 8 and 16). Fail-before on 566de96 (a=16 b=16), pass-after (a=8 b=16).
This commit is contained in:
agra
2026-06-08 17:19:41 +03:00
parent 566de96821
commit eb7636d0f3
8 changed files with 161 additions and 46 deletions

View File

@@ -0,0 +1,29 @@
// A qualified generic type head `ns.Box(args)` must instantiate the template
// AUTHORED by `ns`'s module — not the global same-name template that happened to
// win the last-wins `struct_template_map`. `main` imports two namespaces that
// each author a same-name generic `Box($T)` with a DIFFERENT layout (a: one
// field, b: two fields). `a.Box(s64)` and `b.Box(s64)` must resolve to their OWN
// module's template (sizes 8 and 16) and be DISTINCT types, so a field unique to
// b's layout (`y`) is reachable only through `b.Box`.
//
// This is the ambiguity escape hatch made real: when a bare `Box(s64)` is
// ambiguous (two flat same-name authors), the diagnostic tells the user to
// "qualify the reference"; that advice only works if `ns.Box(..)` actually
// selects ns's author.
//
// Regression (Phase E4): before qualified generic-head selection, the head was
// stripped to the bare name and read from the global `struct_template_map`, so
// `a.Box(s64)` and `b.Box(s64)` both instantiated the last-wins template (both
// size 16) — the namespace qualifier was ignored.
#import "modules/std.sx";
a :: #import "0772-modules-qualified-generic-head-author/a.sx";
b :: #import "0772-modules-qualified-generic-head-author/b.sx";
main :: () -> s32 {
pa : a.Box(s64) = .{ x = 1 };
pb : b.Box(s64) = .{ x = 10, y = 20 };
print("a={} b={}\n", size_of(a.Box(s64)), size_of(b.Box(s64)));
print("pa.x={} pb.x={} pb.y={}\n", pa.x, pb.x, pb.y);
0
}

View File

@@ -0,0 +1,2 @@
// Author A's generic `Box` — one s64 field (size 8).
Box :: struct($T: Type) { x: T; }

View File

@@ -0,0 +1,3 @@
// Author B's generic `Box` — two s64 fields (size 16). Same template NAME as
// A's, different layout: the qualified head must select by namespace author.
Box :: struct($T: Type) { x: T; y: T; }

View File

@@ -0,0 +1,2 @@
a=8 b=16
pa.x=1 pb.x=10 pb.y=20

View File

@@ -422,7 +422,12 @@ enforced at every one of those sites as well: a bare type (including a type-retu
function head) that two or more flat imports each declare — with no own author to function head) that two or more flat imports each declare — with no own author to
win — is **ambiguous and rejected** (`type 'X' is ambiguous: it is declared in win — is **ambiguous and rejected** (`type 'X' is ambiguous: it is declared in
multiple flat-imported modules; qualify the reference or remove the duplicate multiple flat-imported modules; qualify the reference or remove the duplicate
import`) — never a silent pick of one author. (A library's own *internal* type references still resolve: a generic import`) — never a silent pick of one author. Qualifying the reference is a real
escape hatch for a **generic head** too: `ns.Box(args)` selects the template
AUTHORED by `ns`'s module, so two namespaces each declaring a same-name
`Box($T)` with different layouts stay distinct types (`a.Box(s64)` and
`b.Box(s64)` instantiate their own author's fields), never the global last-wins
template. (A library's own *internal* type references still resolve: a generic
struct / pack fn / protocol body is instantiated in the module that defines it, so struct / pack fn / protocol body is instantiated in the module that defines it, so
e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call
site.) site.)

View File

@@ -14148,6 +14148,15 @@ pub const Lowering = struct {
const elem = self.resolveTypeWithBindings(cl.args[1]); const elem = self.resolveTypeWithBindings(cl.args[1]);
return self.module.types.vectorOf(elem, length); 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);
}
}
// User-defined generic struct // User-defined generic struct
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| { if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
if (!is_qualified and self.headTypeLeak(callee_name, cl.callee.span)) return .unresolved; if (!is_qualified and self.headTypeLeak(callee_name, cl.callee.span)) return .unresolved;
@@ -14191,6 +14200,18 @@ 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);
}
}
}
// User-defined generic struct: look up template and instantiate // User-defined generic struct: look up template and instantiate
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| { if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved; if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved;
@@ -14240,6 +14261,23 @@ pub const Lowering = struct {
var name_parts = std.ArrayList(u8).empty; var name_parts = std.ArrayList(u8).empty;
name_parts.appendSlice(self.alloc, tmpl.name) catch {}; name_parts.appendSlice(self.alloc, tmpl.name) catch {};
// A qualified `ns.Box(..)` head can select a generic template whose bare
// name also belongs to a DIFFERENT module's same-name template (the one
// that won the last-wins `struct_template_map`). Both would mangle to
// `Box__s64` and the second instantiation would alias the first's layout.
// Tag the NON-canonical author's mangled name with its source so each
// author's instantiation is a distinct type. The canonical (bare-map)
// author keeps the untagged name — no churn for single-author generics.
if (self.program_index.struct_template_map.get(tmpl.name)) |canon| {
const canon_src = canon.source_file orelse "";
const this_src = tmpl.source_file orelse "";
if (!std.mem.eql(u8, canon_src, this_src)) {
var tag_buf: [24]u8 = undefined;
const tag = std.fmt.bufPrint(&tag_buf, "$m{x}", .{std.hash.Wyhash.hash(0, this_src)}) catch "";
name_parts.appendSlice(self.alloc, tag) catch {};
}
}
// Bind type params to args and build name suffix // Bind type params to args and build name suffix
const saved_type_bindings = self.type_bindings; const saved_type_bindings = self.type_bindings;
const saved_value_bindings = self.comptime_value_bindings; const saved_value_bindings = self.comptime_value_bindings;
@@ -14783,57 +14821,91 @@ pub const Lowering = struct {
}; };
} }
/// Build an owned generic-struct template (type params, field names, field
/// type nodes) for `sd`, pinned to its declaring `source_file`. The returned
/// template is heap-owned via `self.alloc`; callers register it under a bare
/// or namespace-qualified key. Null on OOM.
fn buildGenericStructTemplate(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) ?StructTemplate {
const owned_name = self.alloc.dupe(u8, sd.name) catch return null;
const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return null;
for (sd.type_params, 0..) |tp, i| {
const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
const cname = tp.constraint.data.type_expr.name;
// "Type" or a protocol name → type param
break :blk std.mem.eql(u8, cname, "Type") or
self.program_index.protocol_decl_map.contains(cname) or
self.program_index.protocol_ast_map.contains(cname);
} else false);
tps[i] = .{
.name = self.alloc.dupe(u8, tp.name) catch return null,
// $T: Type, $T: Lerpable, $T: Type/Eq — all are type params.
// `..$Ts: []Type` (variadic) is a type-pack param. Only value
// params like $N: u32 are non-type.
.is_type_param = is_type_param,
.is_variadic = tp.is_variadic,
// Capture a value param's declared type name (`$K: u32` →
// "u32") so instantiation can range-check the folded arg.
.value_type = if (!is_type_param and tp.constraint.data == .type_expr)
(self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null)
else
null,
};
}
const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return null;
for (sd.field_names, 0..) |fn_str, i| {
fnames[i] = self.alloc.dupe(u8, fn_str) catch return null;
}
// Field type nodes are *Node pointers into the AST; copy the slice of
// pointers (the nodes themselves are heap-allocated).
const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return null;
return .{
.name = owned_name,
.type_params = tps,
.field_names = fnames,
.field_type_nodes = ftype_nodes,
.source_file = source_file,
};
}
/// Select the generic struct template AUTHORED by namespace `alias`'s target
/// module (the `importer → alias → NamespaceTarget` edge), not the bare
/// last-wins `struct_template_map`. A qualified head `ns.Box(..)` must
/// instantiate ns's OWN `Box`, even when another module's same-name `Box` won
/// the bare map. Null when the alias is unknown in the current source or its
/// module authors no such generic struct — the caller then falls back to the
/// legacy bare lookup.
fn qualifiedStructTemplate(self: *Lowering, alias: []const u8, member: []const u8) ?StructTemplate {
const edges = self.program_index.namespace_edges orelse return null;
const from = self.current_source_file orelse return null;
const alias_map = edges.getPtr(from) orelse return null;
const target = alias_map.get(alias) orelse return null;
for (target.own_decls) |decl| {
// A top-level struct is authored either as a bare `struct_decl` node
// or a `const_decl` whose value is one (`Box :: struct($T){...}`).
const sd: *const ast.StructDecl = switch (decl.data) {
.struct_decl => |*s| s,
.const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else continue,
else => continue,
};
if (!std.mem.eql(u8, sd.name, member)) continue;
if (sd.type_params.len == 0) continue;
return self.buildGenericStructTemplate(sd, decl.source_file orelse target.target_module_path);
}
return null;
}
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void { fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void {
const table = &self.module.types; const table = &self.module.types;
const name_id = table.internString(sd.name); const name_id = table.internString(sd.name);
// Generic structs: store as owned template, don't resolve fields yet // Generic structs: store as owned template, don't resolve fields yet
if (sd.type_params.len > 0) { if (sd.type_params.len > 0) {
const owned_name = self.alloc.dupe(u8, sd.name) catch return; const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return;
self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {};
// Build owned type_params
const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return;
for (sd.type_params, 0..) |tp, i| {
const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
const cname = tp.constraint.data.type_expr.name;
// "Type" or a protocol name → type param
break :blk std.mem.eql(u8, cname, "Type") or
self.program_index.protocol_decl_map.contains(cname) or
self.program_index.protocol_ast_map.contains(cname);
} else false);
tps[i] = .{
.name = self.alloc.dupe(u8, tp.name) catch return,
// $T: Type, $T: Lerpable, $T: Type/Eq — all are type params.
// `..$Ts: []Type` (variadic) is a type-pack param. Only value
// params like $N: u32 are non-type.
.is_type_param = is_type_param,
.is_variadic = tp.is_variadic,
// Capture a value param's declared type name (`$K: u32` →
// "u32") so instantiation can range-check the folded arg.
.value_type = if (!is_type_param and tp.constraint.data == .type_expr)
(self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null)
else
null,
};
}
// Copy field names
const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return;
for (sd.field_names, 0..) |fn_str, i| {
fnames[i] = self.alloc.dupe(u8, fn_str) catch return;
}
// Field type nodes: these are *Node pointers into the AST.
// Copy the slice of pointers (the nodes themselves are heap-allocated).
const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return;
self.program_index.struct_template_map.put(owned_name, .{
.name = owned_name,
.type_params = tps,
.field_names = fnames,
.field_type_nodes = ftype_nodes,
.source_file = source_file,
}) catch {};
// Register methods under "TemplateName.method" in fn_ast_map // Register methods under "TemplateName.method" in fn_ast_map
for (sd.methods) |method_node| { for (sd.methods) |method_node| {