fix: visibility-aware type-name resolution at registration time
Enum payloads, union fields, inline struct/enum/union field types, and named error-set references now resolve through the visibility-aware `inner` recursion hook (the same seam `resolveCompound` uses) instead of the flat `findByName`. A bare type name in any of these positions now selects the querying module's OWN author over a same-name namespaced import -- the own-wins rule already applied to top-level named references and struct fields. - buildEnumInfo / buildUnionInfo / resolveInlineEnum / resolveInlineStruct / resolveInlineUnion / resolveErrorType take the `inner: anytype` seam; registerEnumDecl / registerUnionDecl and the struct-const annotation pass `self` (visibility-aware); resolveAstType passes the stateless `si`. - resolveTypeWithBindings routes inline type decls and named error refs through `self` instead of delegating to flat resolveAstType. Regression tests: examples/0781 (top-level enum payload over a namespaced import), examples/0784 (inline struct field). Addresses issue 0132's broader latent class; the protocol-return case (0132 primary) is a separate registerProtocolDecl fix and stays open. The error-set reference path is in place but dormant pending error-set per-decl nominal identity (issue 0134).
This commit is contained in:
29
examples/0781-modules-same-name-enum-payload-own-wins.sx
Normal file
29
examples/0781-modules-same-name-enum-payload-own-wins.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Own-wins for ENUM-PAYLOAD type registration over a NAMESPACED import.
|
||||
// Regression (issue 0132, broader class).
|
||||
//
|
||||
// `#import "modules/std.sx"` carries the stdlib `event.Event` struct — it is
|
||||
// NAMESPACED (reachable only as `event.Event`), never flat-visible. This file
|
||||
// ALSO authors its OWN `Event :: struct { code }`, used as the payload of the
|
||||
// enum `Wrap`. The payload type name `Event` must resolve at REGISTRATION to
|
||||
// THIS file's own `Event` (which has `code`), not the namespaced stdlib struct.
|
||||
//
|
||||
// Fail-before: `registerEnumDecl` built the tagged-union body through the
|
||||
// stateless `type_bridge.buildEnumInfo`, whose flat `findByName` picked the
|
||||
// wrong same-name author — `got`'s payload became the stdlib `Event`, so
|
||||
// `e.code` errored "field 'code' not found on type 'Event'". Fixed by threading
|
||||
// the visibility-aware resolver (`*Lowering` as the `resolveInner` hook) through
|
||||
// `buildEnumInfo` / `buildUnionInfo`, matching what `registerStructDecl` already
|
||||
// does for struct fields.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Event :: struct { code: i64; }
|
||||
Wrap :: enum { none; got: Event; }
|
||||
|
||||
main :: () {
|
||||
w : Wrap = .got(.{ code = 7 });
|
||||
if w == {
|
||||
case .got: (e) { print("code={}\n", e.code); }
|
||||
case .none: print("none\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Own-wins for an INLINE struct field's member type over a NAMESPACED import.
|
||||
// Regression (issue 0132's broader class — the inline-decl resolution boundary).
|
||||
//
|
||||
// `Holder.inner` is an inline `struct { e: Event }`. The member type `Event`
|
||||
// must resolve to THIS file's `Event` (which has `code`), not the namespaced
|
||||
// stdlib `event.Event` struct carried by `#import "modules/std.sx"` (reachable
|
||||
// only as `event.Event`, never bare).
|
||||
//
|
||||
// Fail-before: `Lowering.resolveTypeWithBindings` delegated inline `struct_decl`
|
||||
// field types to the FLAT `type_bridge.resolveAstType`, dropping the visibility
|
||||
// context — so `e: Event` resolved via global `findByName` to the stdlib struct
|
||||
// and `h.inner.e.code` errored "field 'code' not found on type 'Event'". Fixed
|
||||
// by routing inline enum/struct/union decls through the `inner` recursion hook
|
||||
// with `self` (visibility-aware), the same own-wins rule top-level decls use.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Event :: struct { code: i64; }
|
||||
Holder :: struct { inner: struct { e: Event; }; }
|
||||
|
||||
main :: () {
|
||||
h : Holder = ---;
|
||||
h.inner.e = .{ code = 5 };
|
||||
print("code={}\n", h.inner.e.code);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
code=7
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
code=5
|
||||
@@ -717,6 +717,14 @@ pub const Lowering = struct {
|
||||
return self.resolveTypeWithBindings(node);
|
||||
}
|
||||
|
||||
/// Bare TYPE-NAME twin of `resolveInner` for callers holding a name rather
|
||||
/// than an AST node (e.g. an error-set reference `!Named`) — routed through
|
||||
/// the visibility-aware `resolveNominalLeaf`, so a same-name-shadowed set
|
||||
/// resolves to the querying module's own author (issue 0132's class).
|
||||
pub fn resolveName(self: *Lowering, name: []const u8) TypeId {
|
||||
return self.resolveNominalLeaf(name, false, null);
|
||||
}
|
||||
|
||||
/// Fixed-array dimension hook for `TypeResolver.resolveCompound`. A literal
|
||||
/// `[16]T` and a named-const `N :: 16; [N]T` must resolve to the SAME length:
|
||||
/// the dimension folds to a compile-time integer (looked up in the comptime /
|
||||
@@ -936,6 +944,28 @@ pub const Lowering = struct {
|
||||
// literal (`(i32, i32)`); validate its elements are types and reject
|
||||
// non-type elements loudly.
|
||||
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
|
||||
// Inline type declarations used as a field type (`x: enum { ... }`,
|
||||
// `x: struct { ... }`, `x: union { ... }`): build their bodies with
|
||||
// THIS lowering as the `inner` recursion hook, so a payload / field
|
||||
// type NAME resolves in the enclosing module's visibility context —
|
||||
// the SAME own-wins-over-namespaced rule the top-level registration
|
||||
// uses (issue 0132's class). Delegating to the flat `else` below
|
||||
// dropped `self`, leaving inline-decl payloads on the global
|
||||
// `findByName` first-match.
|
||||
.enum_decl => return type_bridge.resolveInlineEnum(&node.data.enum_decl, &self.module.types, self),
|
||||
.struct_decl => return type_bridge.resolveInlineStruct(&node.data.struct_decl, &self.module.types, self),
|
||||
.union_decl => return type_bridge.resolveInlineUnion(&node.data.union_decl, &self.module.types, self),
|
||||
// A NAMED error-set reference (`!Named`) resolves its name through
|
||||
// `self` (visibility-aware) too; the bare `!` inferred set has no name
|
||||
// to shadow. NOTE: this reference-side resolution is currently DORMANT
|
||||
// for same-name error-set collisions — error-set DECLARATIONS don't
|
||||
// yet get per-decl nominal identity (E6a covers struct/enum/union
|
||||
// only), so a same-name set collapses to one TypeId at registration
|
||||
// and there is nothing distinct for the reference to select. See issue
|
||||
// 0134; once decls get nominal identity this activates with no change
|
||||
// here. `error_set_decl` is NOT in this switch: it interns only tag
|
||||
// names, resolving no type names, so it stays on the flat `else`.
|
||||
.error_type_expr => return type_bridge.resolveErrorType(&node.data.error_type_expr, &self.module.types, self),
|
||||
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,7 +703,7 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil
|
||||
if (const_node.data == .const_decl) {
|
||||
const cd = const_node.data.const_decl;
|
||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue;
|
||||
const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null;
|
||||
const ty: ?TypeId = if (cd.type_annotation) |ta| self.resolveType(ta) else null;
|
||||
self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {};
|
||||
}
|
||||
}
|
||||
@@ -725,7 +725,12 @@ pub fn registerEnumDecl(self: *Lowering, ed: *const ast.EnumDecl) void {
|
||||
const name_id = table.internString(ed.name);
|
||||
const decl_key: *const anyopaque = @ptrCast(ed);
|
||||
const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id);
|
||||
const info = type_bridge.buildEnumInfo(ed, table, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
// Pass `self` (the visibility-aware `Lowering` resolver) as the `inner`
|
||||
// recursion hook — the same seam `resolveCompound` uses — so a payload type
|
||||
// NAME resolves in the enum's OWN module visibility context (own author wins
|
||||
// over a namespaced same-name import), not via a global `findByName`
|
||||
// first-match (issue 0132's class).
|
||||
const info = type_bridge.buildEnumInfo(ed, table, self);
|
||||
_ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
|
||||
}
|
||||
|
||||
@@ -736,7 +741,8 @@ pub fn registerUnionDecl(self: *Lowering, ud: *const ast.UnionDecl) void {
|
||||
const name_id = table.internString(ud.name);
|
||||
const decl_key: *const anyopaque = @ptrCast(ud);
|
||||
const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id);
|
||||
const info = type_bridge.buildUnionInfo(ud, table, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
// `self` as the visibility-aware `inner` hook — see `registerEnumDecl`.
|
||||
const info = type_bridge.buildUnionInfo(ud, table, self);
|
||||
_ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ const StatelessInner = struct {
|
||||
pub fn resolveInner(self: StatelessInner, node: *const Node) TypeId {
|
||||
return resolveAstType(node, self.table, self.alias_map, self.consts);
|
||||
}
|
||||
/// Bare TYPE-NAME twin of `resolveInner`, for callers that hold a name
|
||||
/// rather than an AST node (an error-set reference `!Named`). Flat:
|
||||
/// registered name → alias → stub, no visibility scoping.
|
||||
pub fn resolveName(self: StatelessInner, name: []const u8) TypeId {
|
||||
return resolveTypeName(name, self.table, self.alias_map, false);
|
||||
}
|
||||
/// Fixed-array dimension at registration time: a literal `[16]T`, a named
|
||||
/// module-global const `N :: 16; [N]T` (typed `N : i64 : 16` too), or a
|
||||
/// constant-foldable expression over those (`[M + 1]`, `[(M + 1) * 2]`).
|
||||
@@ -183,12 +189,15 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap
|
||||
// type — so the omission surfaces; the lowering-side `resolveParamType`
|
||||
// turns it into a real diagnostic.
|
||||
.inferred_type => .unresolved,
|
||||
// Inline type declarations (used as field types)
|
||||
.enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map, consts),
|
||||
.struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map, consts),
|
||||
.union_decl => |ud| resolveInlineUnion(&ud, table, alias_map, consts),
|
||||
// Inline type declarations (used as field types). Enum/union bodies are
|
||||
// built through the shared `inner`-parameterized builders; the stateless
|
||||
// path passes `si` (the `StatelessInner` already constructed above) — the
|
||||
// same `resolveInner` recursion hook `resolveCompound` receives.
|
||||
.enum_decl => |ed| resolveInlineEnum(&ed, table, si),
|
||||
.struct_decl => |sd| resolveInlineStruct(&sd, table, si),
|
||||
.union_decl => |ud| resolveInlineUnion(&ud, table, si),
|
||||
.error_set_decl => |esd| resolveInlineErrorSet(&esd, table),
|
||||
.error_type_expr => |ete| resolveErrorType(&ete, table, alias_map),
|
||||
.error_type_expr => |ete| resolveErrorType(&ete, table, si),
|
||||
else => {
|
||||
// A non-type AST node reached type resolution — a caller bug.
|
||||
// Returning a plausible `.i64` would silently fabricate an 8-byte
|
||||
@@ -340,14 +349,17 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
|
||||
|
||||
// ── Inline type declarations ─────────────────────────────────────────
|
||||
|
||||
/// Stateless inline-enum resolution for a FIELD-type position (`x: enum {...}`):
|
||||
/// the legacy `findByName` short-circuit keeps a single global slot per display
|
||||
/// name. The TOP-LEVEL per-decl nominal identity path (`Lowering.registerEnumDecl`)
|
||||
/// Inline-enum resolution for a FIELD-type position (`x: enum {...}`). Payload
|
||||
/// type NAMES resolve through the injected `inner` recursion hook: the stateless
|
||||
/// `StatelessInner` (flat) when reached from `resolveAstType`, or `*Lowering`
|
||||
/// (visibility-aware) when reached from `Lowering.resolveTypeWithBindings` — so a
|
||||
/// payload name resolves in the enclosing module's context (issue 0132's class).
|
||||
/// The TOP-LEVEL per-decl nominal identity path (`Lowering.registerEnumDecl`)
|
||||
/// shares the body via `buildEnumInfo` but interns under its own nominal id.
|
||||
fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
|
||||
pub fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, inner: anytype) TypeId {
|
||||
const name_id = table.internString(ed.name);
|
||||
if (table.findByName(name_id)) |existing| return existing;
|
||||
const info = buildEnumInfo(ed, table, alias_map, consts);
|
||||
const info = buildEnumInfo(ed, table, inner);
|
||||
const id = table.internNominal(info, 0);
|
||||
table.updatePreservingKey(id, info);
|
||||
return id;
|
||||
@@ -361,7 +373,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
|
||||
/// enum builds a `.tagged_union`; a payload-less enum a plain `.enum`. Nested
|
||||
/// payload structs / variant field types ARE interned here — they are distinct
|
||||
/// nested nominals, not the enum's own identity.
|
||||
pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeInfo {
|
||||
pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, inner: anytype) TypeInfo {
|
||||
const alloc = table.alloc;
|
||||
const name_id = table.internString(ed.name);
|
||||
|
||||
@@ -384,7 +396,7 @@ pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
|
||||
} else {
|
||||
var sfields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
|
||||
for (sd.field_names, sd.field_types) |fname, ftype_node| {
|
||||
const fty = resolveAstType(ftype_node, table, alias_map, consts);
|
||||
const fty = inner.resolveInner(ftype_node);
|
||||
sfields.append(alloc, .{
|
||||
.name = table.internString(fname),
|
||||
.ty = fty,
|
||||
@@ -398,10 +410,10 @@ pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
|
||||
table.updatePreservingKey(field_ty, sinfo);
|
||||
}
|
||||
} else {
|
||||
field_ty = resolveAstType(vt, table, alias_map, consts);
|
||||
field_ty = inner.resolveInner(vt);
|
||||
}
|
||||
} else {
|
||||
field_ty = resolveAstType(vt, table, alias_map, consts);
|
||||
field_ty = inner.resolveInner(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,7 +427,7 @@ pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
|
||||
var backing_type: ?TypeId = null;
|
||||
var tag_type: ?TypeId = null;
|
||||
if (ed.backing_type) |bt| {
|
||||
const backing_ty = resolveAstType(bt, table, alias_map, consts);
|
||||
const backing_ty = inner.resolveInner(bt);
|
||||
backing_type = backing_ty;
|
||||
// Extract tag type from first field of backing struct
|
||||
const backing_info = table.get(backing_ty);
|
||||
@@ -495,7 +507,7 @@ pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
|
||||
if (ed.backing_type) |bt| {
|
||||
// Only use simple backing types (u8, u16, u32, etc.), not struct backing (enum struct)
|
||||
if (bt.data != .struct_decl) {
|
||||
enum_backing = resolveAstType(bt, table, alias_map, consts);
|
||||
enum_backing = inner.resolveInner(bt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +520,14 @@ pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
|
||||
} };
|
||||
}
|
||||
|
||||
fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
|
||||
/// Inline-struct resolution for a FIELD-type position (`x: struct {...}`). Field
|
||||
/// type NAMES resolve through the injected `inner` hook (flat `StatelessInner`
|
||||
/// from `resolveAstType`, or visibility-aware `*Lowering` from
|
||||
/// `resolveTypeWithBindings` — issue 0132's class). The TOP-LEVEL struct path
|
||||
/// (`Lowering.registerStructDecl`) builds its own field list directly via
|
||||
/// `self.resolveType` (it also expands `#using` and qualifies `__anon` names),
|
||||
/// so it does not route through here.
|
||||
pub fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, inner: anytype) TypeId {
|
||||
const alloc = table.alloc;
|
||||
const name_id = table.internString(sd.name);
|
||||
|
||||
@@ -516,7 +535,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map:
|
||||
|
||||
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
|
||||
for (sd.field_names, sd.field_types) |fname, ftype_node| {
|
||||
const field_ty = resolveAstType(ftype_node, table, alias_map, consts);
|
||||
const field_ty = inner.resolveInner(ftype_node);
|
||||
fields.append(alloc, .{
|
||||
.name = table.internString(fname),
|
||||
.ty = field_ty,
|
||||
@@ -531,13 +550,16 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map:
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Stateless inline-union resolution for a FIELD-type position. The TOP-LEVEL
|
||||
/// per-decl nominal identity path (`Lowering.registerUnionDecl`) shares the body
|
||||
/// via `buildUnionInfo` but interns under its own nominal id.
|
||||
fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
|
||||
/// Inline-union resolution for a FIELD-type position. Field type NAMES resolve
|
||||
/// through the injected `inner` hook (flat `StatelessInner` from `resolveAstType`,
|
||||
/// or visibility-aware `*Lowering` from `resolveTypeWithBindings` — issue 0132's
|
||||
/// class). The TOP-LEVEL per-decl nominal identity path
|
||||
/// (`Lowering.registerUnionDecl`) shares the body via `buildUnionInfo` but interns
|
||||
/// under its own nominal id.
|
||||
pub fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, inner: anytype) TypeId {
|
||||
const name_id = table.internString(ud.name);
|
||||
if (table.findByName(name_id)) |existing| return existing;
|
||||
const info = buildUnionInfo(ud, table, alias_map, consts);
|
||||
const info = buildUnionInfo(ud, table, inner);
|
||||
const id = table.internNominal(info, 0);
|
||||
table.updatePreservingKey(id, info);
|
||||
return id;
|
||||
@@ -547,13 +569,13 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: Al
|
||||
/// nominal slot — the shared body-BUILDER behind both the stateless inline
|
||||
/// field-type path (`resolveInlineUnion`) and the stateful per-decl registration
|
||||
/// (`Lowering.registerUnionDecl`).
|
||||
pub fn buildUnionInfo(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeInfo {
|
||||
pub fn buildUnionInfo(ud: *const ast.UnionDecl, table: *TypeTable, inner: anytype) TypeInfo {
|
||||
const alloc = table.alloc;
|
||||
const name_id = table.internString(ud.name);
|
||||
|
||||
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
|
||||
for (ud.field_names, ud.field_types) |fname, ftype_node| {
|
||||
const field_ty = resolveAstType(ftype_node, table, alias_map, consts);
|
||||
const field_ty = inner.resolveInner(ftype_node);
|
||||
fields.append(alloc, .{
|
||||
.name = table.internString(fname),
|
||||
.ty = field_ty,
|
||||
@@ -589,8 +611,8 @@ fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId
|
||||
/// function by the whole-program SCC pass (E1.4); for now every bare `!`
|
||||
/// resolves to the same empty inferred set, which is correct while no
|
||||
/// function raises (E1.3+).
|
||||
fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
|
||||
if (ete.name) |name| return resolveTypeName(name, table, alias_map, false);
|
||||
pub fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable, inner: anytype) TypeId {
|
||||
if (ete.name) |name| return inner.resolveName(name);
|
||||
// `!` is not a legal type/identifier name, so this reserved StringId can
|
||||
// never collide with a user-declared set.
|
||||
const name_id = table.internString("!");
|
||||
|
||||
Reference in New Issue
Block a user