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

@@ -14148,6 +14148,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);
}
}
// User-defined generic struct
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
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
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
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;
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
const saved_type_bindings = self.type_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 {
const table = &self.module.types;
const name_id = table.internString(sd.name);
// Generic structs: store as owned template, don't resolve fields yet
if (sd.type_params.len > 0) {
const owned_name = self.alloc.dupe(u8, sd.name) catch return;
// 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 {};
const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return;
self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {};
// Register methods under "TemplateName.method" in fn_ast_map
for (sd.methods) |method_node| {