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:
3
examples/0192-types-size-of-qualified-alias-mod.sx
Normal file
3
examples/0192-types-size-of-qualified-alias-mod.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Companion module for 0192 — exports `Selection`, a struct whose size is NOT 8
|
||||
// bytes (so a wrong fallback like `.i64` would be detectable).
|
||||
Selection :: struct { a: i32; b: i32; c: i32; } // 12 bytes
|
||||
18
examples/0192-types-size-of-qualified-alias.sx
Normal file
18
examples/0192-types-size-of-qualified-alias.sx
Normal file
@@ -0,0 +1,18 @@
|
||||
// `size_of(alias.Type)` on a module-alias-qualified type must resolve the type,
|
||||
// exactly as a declaration `: alias.Type` does. In argument position the
|
||||
// qualified name parses as a field-access expression; resolveTypeArg now
|
||||
// reconstructs the dotted name and resolves it through the alias map instead of
|
||||
// returning `.unresolved`.
|
||||
//
|
||||
// Regression (issue 0147).
|
||||
#import "modules/std.sx";
|
||||
sel :: #import "0192-types-size-of-qualified-alias-mod.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
// Qualified through the module alias, in a type-arg slot.
|
||||
print("size={}\n", size_of(sel.Selection)); // 12
|
||||
// Same name also resolves in a declaration (already worked).
|
||||
s : sel.Selection = .{ a = 1, b = 2, c = 3 };
|
||||
print("sum={}\n", s.a + s.b + s.c); // 6
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
size=12
|
||||
sum=6
|
||||
68
issues/0147-size-of-qualified-aliased-type-unresolved.md
Normal file
68
issues/0147-size-of-qualified-aliased-type-unresolved.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 0147 — `size_of(alias.Type)` on a module-aliased type fails: "expects a type, got 'unresolved'"
|
||||
|
||||
> **RESOLVED.** In a reflection-builtin argument slot, a module-alias-qualified
|
||||
> type (`sel.Selection`) parses as a `.field_access` expression (not the dotted
|
||||
> `.type_expr` a declaration annotation produces), and neither `isStaticTypeArg`
|
||||
> nor `resolveTypeArg` (src/ir/lower/generic.zig) had a `.field_access` arm — so
|
||||
> the reflection guard rejected it as a non-type. Both now handle the qualified
|
||||
> form: `isStaticTypeArg` recognizes `alias.Type` when `alias` is a namespace
|
||||
> alias whose target authors a type named `Type` (pure decl scan), and
|
||||
> `resolveTypeArg` resolves it via `namespaceAliasTarget` + `resolveNominalLeaf`
|
||||
> in the target module's context (the same mechanism `lowerFieldAccess` uses for
|
||||
> `alias.Type` in value position). Regression test:
|
||||
> `examples/0192-types-size-of-qualified-alias.sx`.
|
||||
|
||||
## Summary
|
||||
`size_of(T)` does not resolve a type referenced through a module ALIAS
|
||||
(`alias :: #import "..."`), even though that same alias resolves fine everywhere
|
||||
else (declarations, casts, struct-literal construction). The compiler reports the
|
||||
qualified type as `unresolved` only inside `size_of`.
|
||||
|
||||
## Repro
|
||||
```sx
|
||||
sel :: #import "doc/selection.sx"; // selection.sx exports `Selection`
|
||||
|
||||
box :: () -> *sel.Selection {
|
||||
// both of these fail:
|
||||
p : *sel.Selection = xx context.allocator.alloc_bytes(size_of(sel.Selection));
|
||||
memset(xx p, 0, size_of(sel.Selection));
|
||||
p
|
||||
}
|
||||
```
|
||||
|
||||
Error:
|
||||
```
|
||||
error: size_of expects a type, got 'unresolved'
|
||||
|
|
||||
| ... alloc_bytes(size_of(sel.Selection));
|
||||
| ^^^^^^^^^^^^^
|
||||
```
|
||||
|
||||
Note the SAME `sel.Selection` resolves correctly in the variable declaration
|
||||
`p : *sel.Selection` and in calls like `sel.selection_create(...)` — only the
|
||||
`size_of(...)` argument position treats the qualified name as unresolved.
|
||||
|
||||
## Expected
|
||||
`size_of(sel.Selection)` resolves the aliased type and yields its size, exactly
|
||||
as `size_of(Selection)` does for an unqualified/flat-imported type.
|
||||
|
||||
## Workaround (clean)
|
||||
Introduce an unqualified local type alias and feed THAT to `size_of`:
|
||||
```sx
|
||||
sel :: #import "doc/selection.sx";
|
||||
Selection :: sel.Selection; // unqualified alias
|
||||
|
||||
box :: () -> *Selection {
|
||||
p : *Selection = xx context.allocator.alloc_bytes(size_of(Selection));
|
||||
memset(xx p, 0, size_of(Selection));
|
||||
p
|
||||
}
|
||||
```
|
||||
`size_of(Selection)` (the unqualified alias) resolves fine. Used in
|
||||
photo `tests/toolbar.sx`'s `box_sel` (the selection model is imported qualified
|
||||
as `sel` there to avoid a `Point` collision with `modules/ui/types.sx`).
|
||||
|
||||
## Impact
|
||||
Minor. Only bites when a type must be reached through a module alias AND its size
|
||||
is needed (heap-boxing a zeroed value of that type). The unqualified-alias
|
||||
workaround is a one-liner and reads clearly.
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user