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 + | ^