fix: visibility-aware type resolution for protocol method signatures
`registerProtocolDecl` resolved each method's param/return type NAME
through the flat, visibility-unaware `type_bridge.resolveAstType`, so a
type name colliding across modules bound to the wrong author. In the
repro the user's `Event` enum collides with the stdlib `event.Event`
struct (pulled in by `modules/std.sx`): the protocol grabbed the stdlib
struct, typed an inferred `g_plat.one_event()` as a fieldless struct,
bound the `case .key_up:(e)` payload to `.unresolved`, and emitted
"enum literal '.escape' has no destination type to resolve against".
Resolve both param and return types through
`resolveTypeInSource(pd.source_file, …)` — the visibility-aware resolver
pinned to the protocol's own declaring module, keeping the `Self → *void`
short-circuit. Brings the non-parameterized path to parity with
`instantiateParamProtocol` and concrete-fn signatures. No silent default:
not-visible / ambiguous names still diagnose and poison with `.unresolved`.
Closes issue 0132 — the protocol-return case left open by f13f4ab (which
fixed the enum/union/inline/error-set registration class). Regression
test: examples/0417-protocols-protocol-return-name-collision.sx.
This commit is contained in:
44
examples/0417-protocols-protocol-return-name-collision.sx
Normal file
44
examples/0417-protocols-protocol-return-name-collision.sx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Protocol method signatures resolve their param/return type NAMES in the
|
||||||
|
// protocol's OWN declaring module (own-wins visibility), so a bare type name
|
||||||
|
// that collides with a same-name namespaced import binds to the local author.
|
||||||
|
//
|
||||||
|
// Here the user's `Event` enum shares its name with the stdlib
|
||||||
|
// `std/event.sx` `Event :: struct` (pulled in, namespaced as `event`, by
|
||||||
|
// `#import "modules/std.sx"`). `Plat.one_event` returns the user's `Event`;
|
||||||
|
// `ev := g_plat.one_event()` infers that type, so the `case .key_up:(e)`
|
||||||
|
// payload binds a `KeyData` and `.escape` resolves against `Keycode`.
|
||||||
|
//
|
||||||
|
// Regression (issue 0132): `registerProtocolDecl` used to resolve method
|
||||||
|
// signature types through the flat, visibility-UNAWARE `type_bridge`
|
||||||
|
// resolver, which picked the stdlib `event.Event` struct instead — typing
|
||||||
|
// `ev` as a fieldless struct, binding `.unresolved`, and emitting
|
||||||
|
// "enum literal '.escape' has no destination type to resolve against". The
|
||||||
|
// fix pins resolution to `pd.source_file`, mirroring the parameterized-
|
||||||
|
// protocol and concrete-fn signature paths.
|
||||||
|
//
|
||||||
|
// Expect: prints `escape!`, exit 0.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Keycode :: enum { unknown; escape; enter; }
|
||||||
|
KeyData :: struct { key: Keycode; }
|
||||||
|
Event :: enum { none; key_up: KeyData; }
|
||||||
|
|
||||||
|
Plat :: protocol { one_event :: () -> Event; }
|
||||||
|
|
||||||
|
Impl :: struct { dummy: i64; }
|
||||||
|
impl Plat for Impl {
|
||||||
|
one_event :: (self: *Impl) -> Event { return .key_up(.{ key = .escape }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
impl : Impl = .{ dummy = 0 };
|
||||||
|
g_plat : Plat = xx @impl;
|
||||||
|
ev := g_plat.one_event(); // type INFERRED from protocol return
|
||||||
|
if ev == {
|
||||||
|
case .key_up: (e) {
|
||||||
|
// `e` is KeyData (payload of the user's Event), `.escape` a Keycode
|
||||||
|
if e.key == .escape { print("escape!\n"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
escape!
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
# 0132 — protocol method return/param type resolves to the WRONG same-name type (visibility-unaware registration)
|
# 0132 — protocol method return/param type resolves to the WRONG same-name type (visibility-unaware registration)
|
||||||
|
|
||||||
|
> **RESOLVED (2026-06-13).** Root cause: `registerProtocolDecl`
|
||||||
|
> (`src/ir/protocols.zig`) resolved each method's param/return type NAME
|
||||||
|
> through the flat, visibility-UNAWARE `type_bridge.resolveAstType`, so a
|
||||||
|
> name colliding across modules (the user's `Event` enum vs the stdlib
|
||||||
|
> `event.Event` struct) bound to the wrong author. Fix: resolve both
|
||||||
|
> through `self.l.resolveTypeInSource(pd.source_file, …)` — the
|
||||||
|
> visibility-aware stateful resolver pinned to the protocol's OWN
|
||||||
|
> declaring module — keeping the `Self → *void` short-circuit. This
|
||||||
|
> brings the non-parameterized path to parity with the parameterized
|
||||||
|
> path (`instantiateParamProtocol`) and concrete-fn signatures, which
|
||||||
|
> already pin to the defining module. The broader enum/union/inline/
|
||||||
|
> error-set registration class was already fixed in `f13f4ab`; this
|
||||||
|
> commit closes the protocol-return case it left open. Regression test:
|
||||||
|
> `examples/0417-protocols-protocol-return-name-collision.sx` (prints
|
||||||
|
> `escape!`, exit 0). The error-set reference path remains dormant
|
||||||
|
> pending error-set per-decl nominal identity (issue 0134).
|
||||||
|
|
||||||
> **ROOT CAUSE CORRECTED (2026-06-13).** The original write-up (kept in
|
> **ROOT CAUSE CORRECTED (2026-06-13).** The original write-up (kept in
|
||||||
> "Original hypothesis" below) guessed this was about an inferred
|
> "Original hypothesis" below) guessed this was about an inferred
|
||||||
> protocol-return enum TypeId "not carrying payload struct field types".
|
> protocol-return enum TypeId "not carrying payload struct field types".
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
// issue 0132 — protocol method return/param type resolves to the WRONG
|
|
||||||
// same-name type (visibility-unaware registration).
|
|
||||||
//
|
|
||||||
// ROOT CAUSE (corrected — see the .md): `registerProtocolDecl` resolves
|
|
||||||
// the method return type `Event` through a flat, visibility-UNAWARE lookup
|
|
||||||
// (type_bridge.resolveAstType → findByName). The user's `Event` enum
|
|
||||||
// collides by NAME with the stdlib `std/event.sx` `Event :: struct`
|
|
||||||
// (pulled in by `#import "modules/std.sx"`, namespaced as `event`). The
|
|
||||||
// flat lookup picks the stdlib struct, so `ev := g_plat.one_event()` is
|
|
||||||
// typed as a fieldless struct; the `case .key_up:(e)` payload then binds
|
|
||||||
// `.unresolved`, and `.escape` has no destination type.
|
|
||||||
//
|
|
||||||
// EXPECT (today): build FAILS —
|
|
||||||
// error: enum literal '.escape' has no destination type to resolve against
|
|
||||||
// EXPECT (after fix): prints `escape!`, exit 0.
|
|
||||||
//
|
|
||||||
// Proof it's a name collision: rename `Event` -> `Evt` everywhere and the
|
|
||||||
// inferred form compiles and prints `escape!`. Annotating
|
|
||||||
// `ev : Event = g_plat.one_event();` also sidesteps it (the annotation
|
|
||||||
// path is visibility-aware). See the .md for the full bisection.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
|
||||||
|
|
||||||
Keycode :: enum { unknown; escape; enter; }
|
|
||||||
KeyData :: struct { key: Keycode; }
|
|
||||||
Event :: enum { none; key_up: KeyData; }
|
|
||||||
|
|
||||||
Plat :: protocol { one_event :: () -> Event; }
|
|
||||||
|
|
||||||
Impl :: struct { dummy: i64; }
|
|
||||||
impl Plat for Impl {
|
|
||||||
one_event :: (self: *Impl) -> Event { return .key_up(.{ key = .escape }); }
|
|
||||||
}
|
|
||||||
|
|
||||||
main :: () {
|
|
||||||
impl : Impl = .{ dummy = 0 };
|
|
||||||
g_plat : Plat = xx @impl;
|
|
||||||
ev := g_plat.one_event(); // type INFERRED from protocol return
|
|
||||||
if ev == {
|
|
||||||
case .key_up: (e) {
|
|
||||||
if e.key == .escape { print("escape!\n"); } // <-- errors here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -284,19 +284,27 @@ pub const ProtocolResolver = struct {
|
|||||||
const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info);
|
const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info);
|
||||||
table.updatePreservingKey(id, struct_info);
|
table.updatePreservingKey(id, struct_info);
|
||||||
|
|
||||||
// Build protocol method info for dispatch
|
// Build protocol method info for dispatch. Resolve each method's
|
||||||
|
// param/return type NAMES in the protocol's OWN declaring module
|
||||||
|
// (`pd.source_file`, stamped by `resolveImports`), via the
|
||||||
|
// visibility-aware stateful resolver — NOT the flat, visibility-unaware
|
||||||
|
// `type_bridge.resolveAstType`. The flat lookup picks the WRONG author
|
||||||
|
// when the type name collides across modules (issue 0132: the user's
|
||||||
|
// `Event` enum vs the stdlib `event.Event` struct pulled in by
|
||||||
|
// `modules/std.sx`). This mirrors the parameterized-protocol path
|
||||||
|
// (`instantiateParamProtocol`, lower/protocol.zig) and concrete-fn
|
||||||
|
// signatures, which already pin to the defining module. `Self` short-
|
||||||
|
// circuits to `*void` before the leaf, as before. `pd.source_file ==
|
||||||
|
// null` (synthesized decl) falls back to the current context.
|
||||||
var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
|
var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
|
||||||
for (pd.methods) |method| {
|
for (pd.methods) |method| {
|
||||||
var ptypes = std.ArrayList(TypeId).empty;
|
var ptypes = std.ArrayList(TypeId).empty;
|
||||||
for (method.params) |p| {
|
for (method.params) |p| {
|
||||||
// Self → *void for protocol context; everything else
|
|
||||||
// goes through `resolveAstType`, threaded with the canonical
|
|
||||||
// alias map (`ProgramIndex.type_alias_map`).
|
|
||||||
const pty = blk: {
|
const pty = blk: {
|
||||||
if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) {
|
if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) {
|
||||||
break :blk void_ptr_ty;
|
break :blk void_ptr_ty;
|
||||||
}
|
}
|
||||||
break :blk type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
|
break :blk self.l.resolveTypeInSource(pd.source_file, p);
|
||||||
};
|
};
|
||||||
ptypes.append(self.l.alloc, pty) catch unreachable;
|
ptypes.append(self.l.alloc, pty) catch unreachable;
|
||||||
}
|
}
|
||||||
@@ -306,7 +314,7 @@ pub const ProtocolResolver = struct {
|
|||||||
ret_is_self = true;
|
ret_is_self = true;
|
||||||
break :blk void_ptr_ty;
|
break :blk void_ptr_ty;
|
||||||
}
|
}
|
||||||
break :blk type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
|
break :blk self.l.resolveTypeInSource(pd.source_file, rt);
|
||||||
} else .void;
|
} else .void;
|
||||||
method_infos.append(self.l.alloc, .{
|
method_infos.append(self.l.alloc, .{
|
||||||
.name = method.name,
|
.name = method.name,
|
||||||
|
|||||||
Reference in New Issue
Block a user