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;
|
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,
|
.pack_index_type_expr,
|
||||||
.pointer_type_expr,
|
.pointer_type_expr,
|
||||||
.many_pointer_type_expr,
|
.many_pointer_type_expr,
|
||||||
@@ -438,6 +467,36 @@ pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
|||||||
.optional_type_expr,
|
.optional_type_expr,
|
||||||
.function_type_expr,
|
.function_type_expr,
|
||||||
=> return self.resolveTypeWithBindings(node),
|
=> 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,
|
else => return .unresolved,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user