From 19bc644b11117e0f4c969c98c680583bb4be74ac Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 29 May 2026 19:34:03 +0300 Subject: [PATCH] lang 2.4: enforce interface-only access on pack elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A protocol-constrained pack element exposes only the constraint protocol's interface (the locked decision): `xs[i].` is rejected unless `` is one of the protocol's methods. `xs[i].v` (a concrete field of IntCell, not declared on Box) now errors, like a constrained generic — even though the substituted element is concretely an IntCell. monomorphizePackFn records the pack param's constraint protocol in a new `pack_constraint` map (pack-name → protocol); lowerFieldAccess checks it on an `xs[i]` (index_expr) base BEFORE substitution erases the "constrained to P" context. Protocol method calls (`xs[i].get()`) pass — the name is in the protocol. Regression: examples/195-pack-interface-only.sx. --- examples/195-pack-interface-only.sx | 22 ++++++++++++ src/ir/lower.zig | 39 ++++++++++++++++++++- tests/expected/195-pack-interface-only.exit | 1 + tests/expected/195-pack-interface-only.txt | 5 +++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 examples/195-pack-interface-only.sx create mode 100644 tests/expected/195-pack-interface-only.exit create mode 100644 tests/expected/195-pack-interface-only.txt diff --git a/examples/195-pack-interface-only.sx b/examples/195-pack-interface-only.sx new file mode 100644 index 0000000..753acbc --- /dev/null +++ b/examples/195-pack-interface-only.sx @@ -0,0 +1,22 @@ +// Feature 1 — a pack element exposes ONLY the constraint protocol's interface. +// `xs[i].v` reaches a concrete field of IntCell that is not part of `Box`, so +// it's rejected even though IntCell does have `v` — a pack element is viewed +// through the protocol, like a constrained generic. (Protocol methods like +// `get()` ARE callable; see examples 193/194.) + +#import "modules/std.sx"; + +Box :: protocol(T: Type) { + get :: () -> T; +} +IntCell :: struct { v: s64; } +impl Box(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; } + +leak :: (..xs: Box) -> s64 { + return xs[0].v; // `v` is not part of Box — error +} + +main :: () -> s32 { + print("{}\n", leak(IntCell.{ v = 5 })); + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 87c7c61..5d042cb 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -178,6 +178,12 @@ pub const Lowering = struct { /// inference resolve `args[i]` to the correct concrete type even /// before the mono's scope is set up. pack_arg_types: ?std.StringHashMap([]const TypeId) = null, + /// Active during a protocol-pack mono's body lowering: pack-param name → + /// constraint protocol name (`..xs: Box` ⇒ `xs` → `"Box"`). Lets + /// `lowerFieldAccess` enforce the interface-only rule — a member access + /// `xs[i].` is rejected unless `` is one of the protocol's methods. + /// Null / absent for the comptime `..$args` pack (no constraint). + pack_constraint: ?std.StringHashMap([]const u8) = null, struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2) foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames @@ -3970,6 +3976,26 @@ pub const Lowering = struct { } } + // Interface-only enforcement (Decision): a member access on a + // constrained pack element `xs[i].` may only name a method of the + // constraint protocol — not an arbitrary concrete field. Checked here, + // on the `xs[i]` (index_expr) base, BEFORE substitution erases the + // "constrained to P" context. Protocol method CALLS go through the call + // path; a method name passes this check (it's in the protocol). + if (self.pack_constraint) |pcon| { + if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) { + const base_name = fa.object.data.index_expr.object.data.identifier.name; + if (pcon.get(base_name)) |proto| { + if (self.lookupProtocolField(proto, fa.field) == null) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto }); + } + return self.builder.constInt(0, .void); + } + } + } + } + // Check for struct constant access: Struct.CONST if (fa.object.data == .identifier) { const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; @@ -8772,13 +8798,18 @@ pub const Lowering = struct { const owned_name = self.alloc.dupe(u8, mangled_name) catch return; self.lowered_functions.put(owned_name, {}) catch {}; - // Find the pack param's name and position in fd.params. + // Find the pack param's name and position in fd.params, plus its + // constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none). var pack_name: []const u8 = ""; var pack_param_idx: usize = std.math.maxInt(usize); + var pack_proto: ?[]const u8 = null; for (fd.params, 0..) |p, i| { if (isPackParam(p)) { pack_name = p.name; pack_param_idx = i; + if (p.is_pack and p.type_expr.data == .type_expr) { + pack_proto = p.type_expr.data.type_expr.name; + } break; } } @@ -8797,6 +8828,7 @@ pub const Lowering = struct { const saved_pan = self.pack_arg_nodes; const saved_ppc = self.pack_param_count; const saved_pat = self.pack_arg_types; + const saved_pcon = self.pack_constraint; const saved_iri = self.inline_return_target; const saved_ctx_ref = self.current_ctx_ref; self.func_defer_base = self.defer_stack.items.len; @@ -8810,6 +8842,7 @@ pub const Lowering = struct { self.pack_arg_nodes = saved_pan; self.pack_param_count = saved_ppc; self.pack_arg_types = saved_pat; + self.pack_constraint = saved_pcon; self.inline_return_target = saved_iri; self.current_ctx_ref = saved_ctx_ref; self.builder.func = saved_func; @@ -8853,9 +8886,13 @@ pub const Lowering = struct { var pre_pat = std.StringHashMap([]const TypeId).init(self.alloc); defer pre_pat.deinit(); pre_pat.put(pack_name, arg_types) catch return; + var pre_pcon = std.StringHashMap([]const u8).init(self.alloc); + defer pre_pcon.deinit(); + if (pack_proto) |proto| pre_pcon.put(pack_name, proto) catch return; self.pack_arg_nodes = pre_pan; self.pack_param_count = pre_ppc; self.pack_arg_types = pre_pat; + self.pack_constraint = if (pack_proto != null) pre_pcon else null; const declared_is_generic_ret = blk: { const rt = fd.return_type orelse break :blk false; diff --git a/tests/expected/195-pack-interface-only.exit b/tests/expected/195-pack-interface-only.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/195-pack-interface-only.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/195-pack-interface-only.txt b/tests/expected/195-pack-interface-only.txt new file mode 100644 index 0000000..0d8656c --- /dev/null +++ b/tests/expected/195-pack-interface-only.txt @@ -0,0 +1,5 @@ +error: 'v' is not part of protocol 'Box' — a pack element exposes only the protocol's interface + --> /Users/agra/projects/sx/examples/195-pack-interface-only.sx:16:12 + | +16 | return xs[0].v; // `v` is not part of Box — error + | ^^^^^^^