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.
This commit is contained in:
agra
2026-05-30 02:41:01 +03:00
parent b48766d153
commit 2f27f93bcf
4 changed files with 123 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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.<name>` projection can target two protocol namespaces:

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
a.get=42
b.get=hi