From fc4d239fddc54f90aa953f832ab9d1b0fb58de3e Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 29 May 2026 18:01:48 +0300 Subject: [PATCH] lang 2.4: enforce protocol-pack conformance per position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each argument bound to a `..xs: P` pack must conform to P — previously the constraint was decorative (any type was accepted). `lowerPackFnCall` now captures the pack param's constraint protocol and checks each pack arg via a new `packArgConformsTo`, which accepts: a plain-protocol impl (`protocol_thunk_map`), any parameterised impl `P() for T` (scan of `param_impl_map` for a `P\x00…\x00mangle(T)` key — the per-element type-args are inferred from the impl, not written out), or an arg already erased to P's own protocol struct. Non-conformers get a per-position error pointing at the argument. Only enforced for a known protocol constraint. Regression: examples/192-pack-non-conform.sx (a struct lacking `impl Show` in a `..xs: Show` pack → diagnostic, exit 1). --- examples/192-pack-non-conform.sx | 24 +++++++++++++ src/ir/lower.zig | 44 ++++++++++++++++++++++++ tests/expected/192-pack-non-conform.exit | 1 + tests/expected/192-pack-non-conform.txt | 5 +++ 4 files changed, 74 insertions(+) create mode 100644 examples/192-pack-non-conform.sx create mode 100644 tests/expected/192-pack-non-conform.exit create mode 100644 tests/expected/192-pack-non-conform.txt diff --git a/examples/192-pack-non-conform.sx b/examples/192-pack-non-conform.sx new file mode 100644 index 0000000..c0976d3 --- /dev/null +++ b/examples/192-pack-non-conform.sx @@ -0,0 +1,24 @@ +// Feature 1 — a pack argument that doesn't conform to the constraint protocol +// is a per-position error. `Naked` has no `impl Show`, so passing it to a +// `..xs: Show` pack is rejected (pointing at the offending argument). + +#import "modules/std.sx"; + +Show :: protocol(T: Type) { + get :: (self: *Self) -> T; +} +IntBox :: struct { v: s64; } +impl Show(s64) for IntBox { get :: (self: *IntBox) -> s64 => self.v; } + +Naked :: struct { x: s64; } // intentionally NOT `impl Show` + +howmany :: (..xs: Show) -> s64 { + return xs.len; +} + +main :: () -> s32 { + a := IntBox.{ v = 1 }; + n := Naked.{ x = 2 }; + print("{}\n", howmany(a, n)); // `n` does not conform to Show + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 63433c8..ec66f01 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -3032,6 +3032,29 @@ 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\x00`). 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; + } + return false; + } + /// Evaluate a compile-time condition for `inline if`. /// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`. fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool { @@ -8586,10 +8609,16 @@ pub const Lowering = struct { var pack_arg_types = std.ArrayList(TypeId).empty; defer pack_arg_types.deinit(self.alloc); var pack_start: usize = call_node.args.len; + // Constraint protocol of the pack param (`..xs: P`), if any. The + // comptime type-pack `..$args` has no constraint to check. + var pack_protocol: ?[]const u8 = null; var fi: usize = 0; for (fd.params) |p| { if (isPackParam(p)) { pack_start = fi; + if (p.is_pack and p.type_expr.data == .type_expr) { + pack_protocol = p.type_expr.data.type_expr.name; + } break; } if (fi >= call_node.args.len) break; @@ -8605,6 +8634,21 @@ pub const Lowering = struct { } } + // Per-position conformance: each pack arg must impl the constraint + // protocol. Only enforced for a known protocol constraint — an unknown + // name (e.g. a plain type used as a pack constraint) is left alone. + if (pack_protocol) |proto| { + if (self.protocol_ast_map.contains(proto)) { + for (call_node.args[pack_start..], pack_arg_types.items) |arg_node, arg_ty| { + if (!self.packArgConformsTo(proto, arg_ty)) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, arg_node.span, "pack argument of type '{s}' does not conform to protocol '{s}'", .{ self.formatTypeName(arg_ty), proto }); + } + } + } + } + } + // Mangle: `__pack__` with comptime values // (if any) folded into a `__ct_` segment per non-pack // comptime param. Distinct call shapes — including different diff --git a/tests/expected/192-pack-non-conform.exit b/tests/expected/192-pack-non-conform.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/192-pack-non-conform.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/192-pack-non-conform.txt b/tests/expected/192-pack-non-conform.txt new file mode 100644 index 0000000..7db177d --- /dev/null +++ b/tests/expected/192-pack-non-conform.txt @@ -0,0 +1,5 @@ +error: pack argument of type 'Naked' does not conform to protocol 'Show' + --> /Users/agra/projects/sx/examples/192-pack-non-conform.sx:22:30 + | +22 | print("{}\n", howmany(a, n)); // `n` does not conform to Show + | ^