diff --git a/examples/0773-modules-qualified-generic-alias-author.sx b/examples/0773-modules-qualified-generic-alias-author.sx new file mode 100644 index 0000000..a43a579 --- /dev/null +++ b/examples/0773-modules-qualified-generic-alias-author.sx @@ -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 +} diff --git a/examples/0773-modules-qualified-generic-alias-author/a.sx b/examples/0773-modules-qualified-generic-alias-author/a.sx new file mode 100644 index 0000000..37242c9 --- /dev/null +++ b/examples/0773-modules-qualified-generic-alias-author/a.sx @@ -0,0 +1,2 @@ +// Author A's generic `Box` — one s64 field (size 8). +Box :: struct($T: Type) { x: T; } diff --git a/examples/0773-modules-qualified-generic-alias-author/b.sx b/examples/0773-modules-qualified-generic-alias-author/b.sx new file mode 100644 index 0000000..e5583af --- /dev/null +++ b/examples/0773-modules-qualified-generic-alias-author/b.sx @@ -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; } diff --git a/examples/expected/0773-modules-qualified-generic-alias-author.exit b/examples/expected/0773-modules-qualified-generic-alias-author.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0773-modules-qualified-generic-alias-author.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0773-modules-qualified-generic-alias-author.stderr b/examples/expected/0773-modules-qualified-generic-alias-author.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0773-modules-qualified-generic-alias-author.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0773-modules-qualified-generic-alias-author.stdout b/examples/expected/0773-modules-qualified-generic-alias-author.stdout new file mode 100644 index 0000000..5a502f6 --- /dev/null +++ b/examples/expected/0773-modules-qualified-generic-alias-author.stdout @@ -0,0 +1,2 @@ +alias a=8 b=16 +ab.x=1 bb.x=10 bb.y=20 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 53389b7..ec751a8 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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);