From 2f27f93bcf62c93ad07bf36b61b6d97f5939fba3 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 30 May 2026 02:41:01 +0300 Subject: [PATCH] lang F1 4.2: parameterized protocol as a runtime value type VL(s64) used as a value/field type resolved to a 0-field stub (size 0); a plain protocol was already a 16-byte {ctx,vtable} value. New instantiateParamProtocol materializes a parameterized protocol per instantiation: a 16-byte protocol value (is_protocol), protocol_decl_map methods resolved under the type-arg binding (get -> T becomes get -> s64 for VL(s64)), a vtable struct, and the type-arg binding recorded for projection. Hooked into resolveParameterizedWithBindings before the empty-struct fallback. xx-erasing a conforming struct into VL(s64)/VL(string) + method dispatch now works (examples/206). This is the keystone for the canonical Combined field (..VL(Ts)). 241 examples + unit green. --- examples/206-parameterized-protocol-value.sx | 22 +++++ src/ir/lower.zig | 98 +++++++++++++++++++ .../206-parameterized-protocol-value.exit | 1 + .../206-parameterized-protocol-value.txt | 2 + 4 files changed, 123 insertions(+) create mode 100644 examples/206-parameterized-protocol-value.sx create mode 100644 tests/expected/206-parameterized-protocol-value.exit create mode 100644 tests/expected/206-parameterized-protocol-value.txt diff --git a/examples/206-parameterized-protocol-value.sx b/examples/206-parameterized-protocol-value.sx new file mode 100644 index 0000000..f60b5d8 --- /dev/null +++ b/examples/206-parameterized-protocol-value.sx @@ -0,0 +1,22 @@ +// Phase 4.2 — parameterized protocol as a runtime VALUE type. `VL(s64)` is a +// 16-byte protocol value {ctx, vtable} (a plain protocol was already, but a +// parameterized one used to resolve to a 0-field stub). A conforming struct +// `xx`-erases into it, and method dispatch uses the bound type-arg +// (`get -> T` becomes `get -> s64` for `VL(s64)`). + +#import "modules/std.sx"; + +VL :: protocol(T: Type) { get :: () -> T; } +IntCell :: struct { v: s64; } +StrCell :: struct { s: string; } +impl VL(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; } +impl VL(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } + +main :: () -> s32 { + a : VL(s64) = xx IntCell.{ v = 42 }; + print("a.get={}\n", a.get()); // 42 (T = s64) + + b : VL(string) = xx StrCell.{ s = "hi" }; + print("b.get={}\n", b.get()); // hi (T = string) + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 7a9a799..182455c 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -11381,6 +11381,14 @@ pub const Lowering = struct { return self.instantiateGenericStruct(tmpl, pt.args); } + // Parameterized protocol used as a value type (`VL(s64)`): materialize a + // 16-byte protocol value with the type-arg bound (not a 0-field stub). + if (self.protocol_ast_map.get(base_name)) |pd| { + if (pd.type_params.len > 0) { + return self.instantiateParamProtocol(pd, pt.args); + } + } + // Fallback: register as named type placeholder const name_id = table.internString(pt.name); return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); @@ -12004,6 +12012,96 @@ pub const Lowering = struct { } } + /// Instantiate a parameterized protocol as a runtime VALUE type: + /// `VL(s64)` → a 16-byte `{ctx, __vtable}` protocol value (`is_protocol`), + /// with method infos resolved under the type-arg binding (so `get -> T` + /// becomes `get -> s64`) and the binding recorded for projection. Cached by + /// the mangled name `VL__s64`. Mirrors the non-parameterized path in + /// `registerProtocolDecl`. + fn instantiateParamProtocol(self: *Lowering, pd: *const ast.ProtocolDecl, args: []const *const Node) TypeId { + const table = &self.module.types; + const void_ptr_ty = table.ptrTo(.void); + + var np = std.ArrayList(u8).empty; + np.appendSlice(self.alloc, pd.name) catch {}; + var tb = std.StringHashMap(TypeId).init(self.alloc); + for (pd.type_params, 0..) |tp, i| { + if (i >= args.len) break; + const ty = self.resolveTypeWithBindings(args[i]); + tb.put(tp.name, ty) catch {}; + np.appendSlice(self.alloc, "__") catch {}; + np.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; + } + const mangled = np.items; + const name_id = table.internString(mangled); + if (table.findByName(name_id)) |existing| { + const info = table.get(existing); + if (info == .@"struct" and info.@"struct".is_protocol) return existing; + } + + // Value struct: {ctx, __vtable} (or ctx + fn-ptrs for an inline protocol). + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + fields.append(self.alloc, .{ .name = table.internString("ctx"), .ty = void_ptr_ty }) catch unreachable; + if (pd.is_inline) { + for (pd.methods) |m| fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable; + } else { + fields.append(self.alloc, .{ .name = table.internString("__vtable"), .ty = void_ptr_ty }) catch unreachable; + } + const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } }; + const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); + table.update(id, struct_info); + + // Method infos resolved with the type-arg binding (T → s64). + const saved_tb = self.type_bindings; + self.type_bindings = tb; + var method_infos = std.ArrayList(ProtocolMethodInfo).empty; + for (pd.methods) |method| { + var ptypes = std.ArrayList(TypeId).empty; + for (method.params) |p| { + const pty = blk: { + if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) break :blk void_ptr_ty; + break :blk self.resolveTypeWithBindings(p); + }; + ptypes.append(self.alloc, pty) catch unreachable; + } + var ret_is_self = false; + const ret = if (method.return_type) |rt| blk: { + if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) { + ret_is_self = true; + break :blk void_ptr_ty; + } + break :blk self.resolveTypeWithBindings(rt); + } else .void; + method_infos.append(self.alloc, .{ + .name = method.name, + .param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable, + .ret_type = ret, + .ret_is_self = ret_is_self, + }) catch unreachable; + } + self.type_bindings = saved_tb; + + const owned = self.alloc.dupe(u8, mangled) catch return id; + self.protocol_decl_map.put(owned, .{ + .name = owned, + .is_inline = pd.is_inline, + .methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, + }) catch {}; + // Record the type-arg binding so projection (`xs.T`, `.value`) and + // method-arg resolution on this instance can recover it. + self.struct_instance_bindings.put(owned, tb) catch {}; + + if (!pd.is_inline) { + var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (pd.methods) |m| vtable_fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable; + var vtable_name_buf: [192]u8 = undefined; + const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{mangled}) catch "__Vtable"; + const vtable_ty = table.intern(.{ .@"struct" = .{ .name = table.internString(vtable_name), .fields = vtable_fields.items } }); + self.protocol_vtable_type_map.put(owned, vtable_ty) catch {}; + } + return id; + } + // ── Pack projection name resolution (Feature 1, Decision 4) ────────── // // A `..pack.` projection can target two protocol namespaces: diff --git a/tests/expected/206-parameterized-protocol-value.exit b/tests/expected/206-parameterized-protocol-value.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/206-parameterized-protocol-value.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/206-parameterized-protocol-value.txt b/tests/expected/206-parameterized-protocol-value.txt new file mode 100644 index 0000000..b440569 --- /dev/null +++ b/tests/expected/206-parameterized-protocol-value.txt @@ -0,0 +1,2 @@ +a.get=42 +b.get=hi