fix: resolve module-alias-qualified type in reflection arg slot (issue 0147)

size_of(sel.Selection) and the other reflection builtins rejected a
module-alias-qualified type: in argument position it parses as a .field_access
expression (not the dotted .type_expr a declaration produces), and neither
isStaticTypeArg nor resolveTypeArg had a .field_access arm. Add both: a pure
namespace-decl scan in isStaticTypeArg, and resolution via namespaceAliasTarget
+ resolveNominalLeaf in the target module context in resolveTypeArg (mirroring
the value-position lowerFieldAccess path). No fabricated-stub fallback.

Regression: examples/0192-types-size-of-qualified-alias.sx
This commit is contained in:
agra
2026-06-21 09:33:46 +03:00
parent c21b683b08
commit 21d91e6718
7 changed files with 152 additions and 0 deletions

View File

@@ -243,6 +243,35 @@ pub fn isStaticTypeArg(self: *Lowering, node: *const Node) bool {
}
return true;
},
.field_access => |fa| {
// A module-alias-qualified type name (`sel.Selection`) is a static
// type iff `fa.object` is a namespace ALIAS (not a runtime scope var)
// whose target module authors a TYPE named `fa.field` (issue 0147).
// Pure predicate: scan the target's own decls — no type resolution
// side effects (the actual TypeId is produced later by
// `resolveTypeArg`'s matching `.field_access` arm).
if (fa.object.data != .identifier) return false;
const oname = fa.object.data.identifier.name;
if (self.scope) |scope| {
if (scope.lookup(oname) != null) return false;
}
const target = self.namespaceAliasTarget(oname, node.span) orelse return false;
for (target.own_decls) |decl| {
const dn = decl.data.declName() orelse continue;
if (!std.mem.eql(u8, dn, fa.field)) continue;
return switch (decl.data) {
.struct_decl, .enum_decl, .union_decl, .error_set_decl => true,
// A const-wrapped type definition or a type alias
// (`Foo :: Bar;` / `Foo :: ns.Bar;`).
.const_decl => |cd| switch (cd.value.data) {
.struct_decl, .enum_decl, .union_decl, .error_set_decl, .identifier, .field_access => true,
else => false,
},
else => false,
};
}
return false;
},
.pack_index_type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
@@ -438,6 +467,36 @@ pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
.optional_type_expr,
.function_type_expr,
=> return self.resolveTypeWithBindings(node),
// A module-alias-qualified type name in a type-arg slot
// (`size_of(sel.Selection)`) parses as a field-access EXPRESSION — unlike
// the dotted `.type_expr` a declaration annotation produces — so without
// this arm it fell through to `else` and resolved to `.unresolved`
// (issue 0147). Reconstruct the qualified `obj.field` name and resolve it
// through the same alias map a declaration uses. Look it up EXPLICITLY
// (findByName + alias map) rather than via `resolveNamed`, whose
// empty-struct-stub fallback would silently fabricate a 0-sized type for
// an unregistered name (the silent-default trap) — a failed lookup must
// surface as a diagnostic + `.unresolved`.
.field_access => |fa| {
// Resolve the member as a TYPE in the alias's TARGET module context —
// the same mechanism `lowerFieldAccess` uses for `alias.Type` in value
// position (src/ir/lower/expr.zig): the alias edge authorizes the reach,
// so set the current source to the target module and resolve the bare
// member name through the source-aware nominal leaf.
if (fa.object.data == .identifier) {
if (self.namespaceAliasTarget(fa.object.data.identifier.name, node.span)) |target| {
const saved_src = self.current_source_file;
self.setCurrentSourceFile(target.target_module_path);
const ty = self.resolveNominalLeaf(fa.field, false, node.span);
self.setCurrentSourceFile(saved_src);
if (ty != .unresolved) return ty;
}
}
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "unresolved qualified type in type-argument position", .{});
}
return .unresolved;
},
else => return .unresolved,
}
}