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:
29
examples/0772-modules-qualified-generic-head-author.sx
Normal file
29
examples/0772-modules-qualified-generic-head-author.sx
Normal 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
|
||||||
|
}
|
||||||
2
examples/0772-modules-qualified-generic-head-author/a.sx
Normal file
2
examples/0772-modules-qualified-generic-head-author/a.sx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Author A's generic `Box` — one s64 field (size 8).
|
||||||
|
Box :: struct($T: Type) { x: T; }
|
||||||
3
examples/0772-modules-qualified-generic-head-author/b.sx
Normal file
3
examples/0772-modules-qualified-generic-head-author/b.sx
Normal 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; }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
a=8 b=16
|
||||||
|
pa.x=1 pb.x=10 pb.y=20
|
||||||
@@ -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.)
|
||||||
|
|||||||
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]);
|
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| {
|
||||||
|
|||||||
Reference in New Issue
Block a user