diff --git a/examples/0192-types-size-of-qualified-alias-mod.sx b/examples/0192-types-size-of-qualified-alias-mod.sx new file mode 100644 index 00000000..e5b5437f --- /dev/null +++ b/examples/0192-types-size-of-qualified-alias-mod.sx @@ -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 diff --git a/examples/0192-types-size-of-qualified-alias.sx b/examples/0192-types-size-of-qualified-alias.sx new file mode 100644 index 00000000..8a631836 --- /dev/null +++ b/examples/0192-types-size-of-qualified-alias.sx @@ -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; +} diff --git a/examples/expected/0192-types-size-of-qualified-alias.exit b/examples/expected/0192-types-size-of-qualified-alias.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0192-types-size-of-qualified-alias.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0192-types-size-of-qualified-alias.stderr b/examples/expected/0192-types-size-of-qualified-alias.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0192-types-size-of-qualified-alias.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0192-types-size-of-qualified-alias.stdout b/examples/expected/0192-types-size-of-qualified-alias.stdout new file mode 100644 index 00000000..4ce09bd7 --- /dev/null +++ b/examples/expected/0192-types-size-of-qualified-alias.stdout @@ -0,0 +1,2 @@ +size=12 +sum=6 diff --git a/issues/0147-size-of-qualified-aliased-type-unresolved.md b/issues/0147-size-of-qualified-aliased-type-unresolved.md new file mode 100644 index 00000000..cae3a732 --- /dev/null +++ b/issues/0147-size-of-qualified-aliased-type-unresolved.md @@ -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. diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 12d8b545..f86e53f1 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -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, } }