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:
22
examples/195-pack-interface-only.sx
Normal file
22
examples/195-pack-interface-only.sx
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
1
tests/expected/195-pack-interface-only.exit
Normal file
1
tests/expected/195-pack-interface-only.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
5
tests/expected/195-pack-interface-only.txt
Normal file
5
tests/expected/195-pack-interface-only.txt
Normal file
@@ -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
|
||||
| ^^^^^^^
|
||||
Reference in New Issue
Block a user