lang 2.4: protocol-interface method calls on pack elements + conformance fix

Calling a protocol method on a pack element now works: `xs[i].greet()` on a
`..xs: Greeter` pack dispatches to the concrete element's impl, and elements
may be heterogeneous (Dog, Cat). This is the protocol-interface access the
pack is for. (Protocol method decls omit the implicit `self`; impls list it —
the earlier malformed `(self: *Self)` decls were why dispatch looked broken.)

Also fixes packArgConformsTo for non-parameterised protocols: it queried
`protocol_thunk_map`, which is only populated lazily when a protocol VALUE is
built with `xx`, so it false-negatived valid conformers. Now it queries
impl-declaration state directly — `param_impl_map` for parameterised protocols,
or `<ty>.<method>` entries in `fn_ast_map` for non-parameterised ones.

examples/193-protocol-pack-methods.sx (heterogeneous Dog+Cat pack, per-element
greet(), order-independent).
This commit is contained in:
agra
2026-05-29 18:53:32 +03:00
parent fc4d239fdd
commit a67627a691
4 changed files with 65 additions and 13 deletions

View File

@@ -0,0 +1,31 @@
// Feature 1 — protocol-interface method calls on heterogeneous pack elements.
//
// `..xs: Greeter` binds per call shape; each `xs[i]` is the concrete element,
// and calling the protocol's own method `greet()` on it dispatches to that
// element's impl. Elements may be DIFFERENT concrete types (Dog, Cat) as long
// as each conforms to Greeter — this is the protocol-interface access the
// pack is for. (Protocol method decls omit the implicit `self`; impls list it.)
#import "modules/std.sx";
Greeter :: protocol {
greet :: () -> s64;
}
Dog :: struct { age: s64; }
Cat :: struct { lives: s64; }
impl Greeter for Dog { greet :: (self: *Dog) -> s64 => self.age; }
impl Greeter for Cat { greet :: (self: *Cat) -> s64 => self.lives * 100; }
pair_sum :: (..xs: Greeter) -> s64 {
return xs[0].greet() + xs[1].greet();
}
main :: () -> s32 {
d := Dog.{ age = 3 };
c := Cat.{ lives = 9 };
print("dog+cat={}\n", pair_sum(d, c)); // 3 + 900 = 903 (heterogeneous)
print("cat+dog={}\n", pair_sum(c, d)); // 900 + 3 = 903 (order swapped)
print("dog+dog={}\n", pair_sum(d, Dog.{ age = 4 })); // 3 + 4 = 7
0;
}

View File

@@ -3032,27 +3032,44 @@ pub const Lowering = struct {
return self.protocol_thunk_map.contains(thunk_key);
}
/// Does `ty` conform to protocol `p_name` under SOME type-args? Used to
/// check protocol-pack elements (`..xs: P`), where each element's
/// protocol type-args are inferred from its impl rather than written out.
/// Covers plain protocols (`protocol_thunk_map`) and parameterised ones
/// (any `param_impl_map` key `P\x00<args>\x00<mangle(ty)>`). An arg already
/// of the protocol's own (erased) type trivially conforms.
/// Does `ty` conform to protocol `p_name` (under SOME type-args for a
/// parameterised protocol)? Used to check protocol-pack elements
/// (`..xs: P`), where each element's protocol type-args are inferred from
/// its impl rather than written out.
///
/// Conformance is queried at the IMPL-DECLARATION level (not via
/// `protocol_thunk_map`, which is only populated lazily when a protocol
/// VALUE is created with `xx`):
/// - Parameterised `P`: any `param_impl_map` key `P\x00<args>\x00<mangle(ty)>`.
/// - Non-parameterised `P`: every required (non-default) method `m` is
/// registered as `<ty>.<m>` in `fn_ast_map` (how `registerImplBlock`
/// records a non-parameterised impl).
/// An arg already of the protocol's own (erased) type trivially conforms.
fn packArgConformsTo(self: *Lowering, p_name: []const u8, ty: TypeId) bool {
if (self.hasImplPlain(p_name, ty)) return true;
// Arg already erased to the protocol struct itself (e.g. `xx a`).
if (!ty.isBuiltin()) {
const info = self.module.types.get(ty);
if (info == .@"struct" and info.@"struct".is_protocol and
std.mem.eql(u8, self.module.types.getString(info.@"struct".name), p_name)) return true;
}
const prefix = std.fmt.allocPrint(self.alloc, "{s}\x00", .{p_name}) catch return false;
const suffix = std.fmt.allocPrint(self.alloc, "\x00{s}", .{self.mangleTypeName(ty)}) catch return false;
var it = self.param_impl_map.keyIterator();
while (it.next()) |k| {
if (std.mem.startsWith(u8, k.*, prefix) and std.mem.endsWith(u8, k.*, suffix)) return true;
const pd = self.protocol_ast_map.get(p_name) orelse return false;
if (pd.type_params.len > 0) {
const prefix = std.fmt.allocPrint(self.alloc, "{s}\x00", .{p_name}) catch return false;
const suffix = std.fmt.allocPrint(self.alloc, "\x00{s}", .{self.mangleTypeName(ty)}) catch return false;
var it = self.param_impl_map.keyIterator();
while (it.next()) |k| {
if (std.mem.startsWith(u8, k.*, prefix) and std.mem.endsWith(u8, k.*, suffix)) return true;
}
return false;
}
return false;
// Non-parameterised: require each non-default method as `<ty>.<m>`.
const ty_name = self.formatTypeName(ty);
for (pd.methods) |m| {
if (m.default_body != null) continue;
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ty_name, m.name }) catch return false;
if (!self.fn_ast_map.contains(q)) return false;
}
return true;
}
/// Evaluate a compile-time condition for `inline if`.

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
dog+cat=903
cat+dog=903
dog+dog=7