lang 2.4: enforce interface-only access on pack elements

A protocol-constrained pack element exposes only the constraint protocol's
interface (the locked decision): `xs[i].<member>` is rejected unless `<member>`
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.
This commit is contained in:
agra
2026-05-29 19:34:03 +03:00
parent e604868ffb
commit 19bc644b11
4 changed files with 66 additions and 1 deletions

View File

@@ -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].<m>` is rejected unless `<m>` 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].<m>` 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;