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

The const-decl alias-registration path treated a qualified generic head
(`ABox :: a.Box(s64)`) only as a gate exemption, then read the bare last-wins
`struct_template_map` — so `ABox` and `BBox` both instantiated whichever
same-name template won globally (both size 16). attempt-9 routed the annotation
head sites through `qualifiedStructTemplate`; this applies the same selection to
the two alias-registration branches (.call and .parameterized_type_expr) before
the bare fallback, and extracts the shared instantiate-and-register logic into
`registerGenericStructAlias`.

ABox :: a.Box(s64) now resolves to a's template (size 8); BBox :: b.Box(s64) to
b's (size 16). Regression 0773 pins it (fail-before alias a=16 b=16, after a=8
b=16).
This commit is contained in:
agra
2026-06-08 17:56:29 +03:00
parent eb7636d0f3
commit 8c59acbd25
7 changed files with 83 additions and 36 deletions

View File

@@ -0,0 +1,28 @@
// A type alias whose RHS is a qualified generic head `ns.Box(args)` must
// instantiate the template AUTHORED by `ns`'s module — not the global same-name
// template that won the last-wins `struct_template_map`. Two namespaces each
// author a same-name generic `Box($T)` with a DIFFERENT layout (a: one field,
// b: two fields). `ABox :: a.Box(s64)` and `BBox :: b.Box(s64)` must register
// aliases over their OWN module's template (sizes 8 and 16) and stay DISTINCT,
// so the field unique to b's layout (`y`) is reachable only through `BBox`.
//
// Regression (Phase E4): before the alias-registration path selected the
// qualified author, the const-decl alias `.call` branch stripped the head to the
// bare name and read the global `struct_template_map`, so `ABox` and `BBox` both
// instantiated the last-wins template (both size 16). attempt-9 fixed the
// annotation head sites; this pins the alias-registration sibling.
#import "modules/std.sx";
a :: #import "0773-modules-qualified-generic-alias-author/a.sx";
b :: #import "0773-modules-qualified-generic-alias-author/b.sx";
ABox :: a.Box(s64);
BBox :: b.Box(s64);
main :: () -> s32 {
ab : ABox = .{ x = 1 };
bb : BBox = .{ x = 10, y = 20 };
print("alias a={} b={}\n", size_of(ABox), size_of(BBox));
print("ab.x={} bb.x={} bb.y={}\n", ab.x, bb.x, bb.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 alias head must select by namespace author.
Box :: struct($T: Type) { x: T; y: T; }

View File

@@ -0,0 +1,2 @@
alias a=8 b=16
ab.x=1 bb.x=10 bb.y=20

View File

@@ -1022,8 +1022,18 @@ pub const Lowering = struct {
// A namespaced callee (`ns.Box(..)`) is an explicit qualified
// reach, exempt from the bare-head visibility gate (E4).
const head_qualified = call_data.callee.data == .field_access;
// A qualified head `ABox :: a.Box(s64)` selects a's OWN
// template via the namespace edge (mirrors the annotation
// head site `resolveTypeCallWithBindings`), not the bare
// last-wins `struct_template_map`.
const qual_call_tmpl: ?StructTemplate = if (head_qualified and call_data.callee.data.field_access.object.data == .identifier)
self.qualifiedStructTemplate(call_data.callee.data.field_access.object.data.identifier.name, callee_name)
else
null;
if (callee_name.len > 0) {
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| reg: {
if (qual_call_tmpl) |qt| {
self.registerGenericStructAlias(cd.name, &qt, call_data.args);
} else if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| reg: {
// 2-hop generic-struct head leak: poison the alias with
// `.unresolved` (suppressed downstream) so the use site
// sees no fabricated-stub cascade, only the loud
@@ -1032,25 +1042,7 @@ pub const Lowering = struct {
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
break :reg;
}
const inst_id = self.instantiateGenericStruct(tmpl, call_data.args);
// Register under the alias name
const alias_name_id = self.module.types.internString(cd.name);
const inst_info = self.module.types.get(inst_id);
if (inst_info == .@"struct") {
const alias_info: types.TypeInfo = .{ .@"struct" = .{
.name = alias_name_id,
.fields = inst_info.@"struct".fields,
} };
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
self.module.types.updatePreservingKey(alias_id, alias_info);
// A generic-struct instantiation alias IS a type
// author: route it through the unified writer so it
// lands in `type_aliases_by_source` and the bare-TYPE
// gate treats it like any other alias (a ns-only
// `Secret :: Box(s32)` is rejected, a flat one
// resolves to the same TypeId `findByName` would).
self.putTypeAlias(self.current_source_file, cd.name, alias_id);
}
self.registerGenericStructAlias(cd.name, tmpl, call_data.args);
} else if (std.mem.eql(u8, callee_name, "Vector")) {
// Builtin type constructor — checked BEFORE
// the generic `fn_ast_map` branch because
@@ -1079,26 +1071,22 @@ pub const Lowering = struct {
const pt = &cd.value.data.parameterized_type_expr;
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
const pt_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null;
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| reg: {
// A qualified base `ABox :: a.Box(s64)` selects a's OWN
// template via the namespace edge (mirrors the annotation
// head site `resolveParameterizedWithBindings`), not the
// bare last-wins `struct_template_map`.
const qual_pt_tmpl: ?StructTemplate = if (pt_qualified) blk: {
const dot = std.mem.indexOfScalar(u8, pt.name, '.').?;
break :blk self.qualifiedStructTemplate(pt.name[0..dot], base_name);
} else null;
if (qual_pt_tmpl) |qt| {
self.registerGenericStructAlias(cd.name, &qt, pt.args);
} else if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| reg: {
if (!pt_qualified and self.headTypeLeak(base_name, cd.value.span)) {
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
break :reg;
}
const inst_id = self.instantiateGenericStruct(tmpl, pt.args);
const alias_name_id = self.module.types.internString(cd.name);
const inst_info = self.module.types.get(inst_id);
if (inst_info == .@"struct") {
const alias_info: types.TypeInfo = .{ .@"struct" = .{
.name = alias_name_id,
.fields = inst_info.@"struct".fields,
} };
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
self.module.types.updatePreservingKey(alias_id, alias_info);
// Same as the `.call` generic-struct branch: a
// parameterized-struct alias is a type author and
// must reach `type_aliases_by_source` so it gates.
self.putTypeAlias(self.current_source_file, cd.name, alias_id);
}
self.registerGenericStructAlias(cd.name, tmpl, pt.args);
} else {
// Builtin parameterised type (Vector(N, T) etc) —
// resolve via type_bridge and register the result
@@ -14898,6 +14886,28 @@ pub const Lowering = struct {
return null;
}
/// Instantiate a generic struct template and register the result under an
/// alias name (`Vec3 :: Vec(3, f32)` / `ABox :: a.Box(s64)`). Shared by the
/// `.call` and `.parameterized_type_expr` const-decl alias branches and the
/// qualified-head selection that precedes the bare `struct_template_map`
/// fallback in each.
fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void {
const inst_id = self.instantiateGenericStruct(tmpl, args);
const alias_name_id = self.module.types.internString(alias_name);
const inst_info = self.module.types.get(inst_id);
if (inst_info != .@"struct") return;
const alias_info: types.TypeInfo = .{ .@"struct" = .{
.name = alias_name_id,
.fields = inst_info.@"struct".fields,
} };
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
self.module.types.updatePreservingKey(alias_id, alias_info);
// A generic-struct instantiation alias IS a type author: route it through
// the unified writer so it lands in `type_aliases_by_source` and the
// bare-TYPE gate treats it like any other alias.
self.putTypeAlias(self.current_source_file, alias_name, alias_id);
}
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);