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:
162
src/ir/lower.zig
162
src/ir/lower.zig
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user