refactor(ir): move protocol/impl registration into ProtocolResolver (A4.2 registration increment)
Move the registration functions behind the protocols.zig facade, per PLAN-ARCH
A4.2 ("then registration", keeping IR emission in Lowering):
- registerProtocolDecl (protocol struct + dispatch method table + vtable type),
- registerImplBlock (concrete impl -> <Target>.<method> in fn_ast_map + default-
method synthesis),
- registerParamImpl (parameterised impl -> param_impl_map / param_impl_pack_map
+ the same-file duplicate diagnostic),
- synthesizeDefaultMethod (facade-private; its only caller moved too).
Moved verbatim with self. -> self.l. facade rewrites. Emission stays in
Lowering: the registry calls self.l.declareFunction (the extern-stub primitive)
but the thunk/value builders (createProtocolThunk / buildProtocolValue /
tryUserConversion / getOrCreateThunks) are NOT moved.
Lowering keeps registerProtocolDecl as a thin pub wrapper (scan pass + 7
unit-test callers); registerImplBlock / registerParamImpl /
synthesizeDefaultMethod deleted (no fallback), the 2 scan call sites routed
through protocolResolver(). New pub: declareFunction (8 callers, emission infra),
ParamImplEntry / PackParamImplEntry (the registry constructs them; stay as
Lowering nested types). State maps remain on Lowering; the facade reads/writes
self.l.* (migrate once planning lands).
protocols.test.zig +2: registerImplBlock records Circle.draw in fn_ast_map (and
packArgConformsTo then sees it); registerParamImpl flags a same-file duplicate
impl Into(s64) for IntCell (the 0412-class, unit level).
zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
churn; the 0410/0411/0412 rejection diagnostics are byte-for-byte preserved.
This commit is contained in:
354
src/ir/lower.zig
354
src/ir/lower.zig
@@ -250,7 +250,7 @@ pub const Lowering = struct {
|
|||||||
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
|
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
|
||||||
/// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled).
|
/// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled).
|
||||||
/// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics.
|
/// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics.
|
||||||
const ParamImplEntry = struct {
|
pub const ParamImplEntry = struct {
|
||||||
methods: []const *const ast.FnDecl,
|
methods: []const *const ast.FnDecl,
|
||||||
source_ty: TypeId,
|
source_ty: TypeId,
|
||||||
target_args: []const TypeId,
|
target_args: []const TypeId,
|
||||||
@@ -272,7 +272,7 @@ pub const Lowering = struct {
|
|||||||
/// (e.g. "args") and the source's return type binds to `ret_var_name`
|
/// (e.g. "args") and the source's return type binds to `ret_var_name`
|
||||||
/// (e.g. "R") when the impl's return is generic. `ret_var_name == null`
|
/// (e.g. "R") when the impl's return is generic. `ret_var_name == null`
|
||||||
/// means the return type is concrete and must match exactly.
|
/// means the return type is concrete and must match exactly.
|
||||||
const PackParamImplEntry = struct {
|
pub const PackParamImplEntry = struct {
|
||||||
methods: []const *const ast.FnDecl,
|
methods: []const *const ast.FnDecl,
|
||||||
source_pack_ty: TypeId,
|
source_pack_ty: TypeId,
|
||||||
target_args: []const TypeId,
|
target_args: []const TypeId,
|
||||||
@@ -968,7 +968,7 @@ pub const Lowering = struct {
|
|||||||
self.registerProtocolDecl(&decl.data.protocol_decl);
|
self.registerProtocolDecl(&decl.data.protocol_decl);
|
||||||
},
|
},
|
||||||
.impl_block => {
|
.impl_block => {
|
||||||
self.registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
||||||
},
|
},
|
||||||
.foreign_class_decl => {
|
.foreign_class_decl => {
|
||||||
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
|
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
|
||||||
@@ -1201,7 +1201,7 @@ pub const Lowering = struct {
|
|||||||
self.registerProtocolDecl(&decl.data.protocol_decl);
|
self.registerProtocolDecl(&decl.data.protocol_decl);
|
||||||
},
|
},
|
||||||
.impl_block => {
|
.impl_block => {
|
||||||
self.registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
||||||
},
|
},
|
||||||
.foreign_class_decl => {
|
.foreign_class_decl => {
|
||||||
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
|
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
|
||||||
@@ -1477,7 +1477,7 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Declare a function as an extern stub (signature only, no body).
|
/// Declare a function as an extern stub (signature only, no body).
|
||||||
fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void {
|
pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void {
|
||||||
// Skip generic templates — they're monomorphized on demand, not declared as extern
|
// Skip generic templates — they're monomorphized on demand, not declared as extern
|
||||||
if (fd.type_params.len > 0) return;
|
if (fd.type_params.len > 0) return;
|
||||||
|
|
||||||
@@ -12972,117 +12972,11 @@ pub const Lowering = struct {
|
|||||||
/// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... }
|
/// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... }
|
||||||
/// Non-inline protocols: { ctx: *void, __vtable: *void }
|
/// Non-inline protocols: { ctx: *void, __vtable: *void }
|
||||||
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
|
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
|
||||||
|
/// Register a protocol declaration. Thin delegation to the canonical owner
|
||||||
|
/// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` as a `pub`
|
||||||
|
/// entry point because the scan pass + several unit tests reach it here.
|
||||||
pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
|
pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
|
||||||
// Decision 4 soft-convention warning: a type-arg and a method (the
|
return self.protocolResolver().registerProtocolDecl(pd);
|
||||||
// "runtime accessor" namespace — protocols have no fields) sharing a
|
|
||||||
// name is allowed, but `..pack.<name>` then resolves by *position*
|
|
||||||
// rather than by precedence, which surprises readers. Alert at decl.
|
|
||||||
for (pd.type_params) |tp| {
|
|
||||||
for (pd.methods) |m| {
|
|
||||||
if (std.mem.eql(u8, tp.name, m.name)) {
|
|
||||||
if (self.diagnostics) |diags| {
|
|
||||||
diags.addFmt(.warn, null, "protocol '{s}' declares type-arg and method both named '{s}'; `..pack.{s}` resolves by position (type-arg in type position, method in value position)", .{ pd.name, tp.name, tp.name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameterised protocols are compile-time-only — no vtable, no boxed
|
|
||||||
// instance struct. Methods reference unbound type params (e.g.
|
|
||||||
// `convert :: () -> Target`) that only get a concrete TypeId per
|
|
||||||
// (Source, Target) pair at xx resolution time. Stash the AST so
|
|
||||||
// `param_impl_map` lookup can resolve method signatures lazily.
|
|
||||||
if (pd.type_params.len > 0) {
|
|
||||||
self.program_index.protocol_ast_map.put(pd.name, pd) catch {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const table = &self.module.types;
|
|
||||||
const name_id = table.internString(pd.name);
|
|
||||||
|
|
||||||
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
||||||
|
|
||||||
// First field: ctx: *void
|
|
||||||
const void_ptr_ty = table.ptrTo(.void);
|
|
||||||
fields.append(self.alloc, .{
|
|
||||||
.name = table.internString("ctx"),
|
|
||||||
.ty = void_ptr_ty,
|
|
||||||
}) catch unreachable;
|
|
||||||
|
|
||||||
if (pd.is_inline) {
|
|
||||||
// One fn-ptr field per protocol method
|
|
||||||
for (pd.methods) |method| {
|
|
||||||
fields.append(self.alloc, .{
|
|
||||||
.name = table.internString(method.name),
|
|
||||||
.ty = void_ptr_ty, // fn ptrs are opaque pointers
|
|
||||||
}) catch unreachable;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Vtable pointer
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Build protocol method info for dispatch
|
|
||||||
var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
|
|
||||||
for (pd.methods) |method| {
|
|
||||||
var ptypes = std.ArrayList(TypeId).empty;
|
|
||||||
for (method.params) |p| {
|
|
||||||
// Self → *void for protocol context; everything else
|
|
||||||
// goes through `resolveAstType`, threaded with the canonical
|
|
||||||
// alias map (`ProgramIndex.type_alias_map`).
|
|
||||||
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 type_bridge.resolveAstType(p, table, &self.program_index.type_alias_map);
|
|
||||||
};
|
|
||||||
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 type_bridge.resolveAstType(rt, table, &self.program_index.type_alias_map);
|
|
||||||
} 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.program_index.protocol_decl_map.put(pd.name, .{
|
|
||||||
.name = pd.name,
|
|
||||||
.is_inline = pd.is_inline,
|
|
||||||
.methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable,
|
|
||||||
}) catch {};
|
|
||||||
self.program_index.protocol_ast_map.put(pd.name, pd) catch {};
|
|
||||||
|
|
||||||
// For vtable protocols, create the vtable struct type
|
|
||||||
if (!pd.is_inline) {
|
|
||||||
var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
||||||
for (pd.methods) |method| {
|
|
||||||
vtable_fields.append(self.alloc, .{
|
|
||||||
.name = table.internString(method.name),
|
|
||||||
.ty = void_ptr_ty,
|
|
||||||
}) catch unreachable;
|
|
||||||
}
|
|
||||||
var vtable_name_buf: [128]u8 = undefined;
|
|
||||||
const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable";
|
|
||||||
const vtable_name_id = table.internString(vtable_name);
|
|
||||||
const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } };
|
|
||||||
const vtable_ty = table.intern(vtable_info);
|
|
||||||
self.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instantiate a parameterized protocol as a runtime VALUE type:
|
/// Instantiate a parameterized protocol as a runtime VALUE type:
|
||||||
@@ -13545,236 +13439,6 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register an impl block: register its methods as TypeName.method in fn_ast_map.
|
|
||||||
fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void {
|
|
||||||
// Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`):
|
|
||||||
// record into `param_impl_map` for compile-time resolution by `lowerXX`.
|
|
||||||
// Methods are NOT registered in fn_ast_map — they're monomorphised lazily
|
|
||||||
// per (Source, Target) pair at the xx call site.
|
|
||||||
if (ib.protocol_type_args.len > 0) {
|
|
||||||
self.registerParamImpl(ib, decl, is_imported);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Collect explicitly implemented method names
|
|
||||||
var impl_methods = std.StringHashMap(void).init(self.alloc);
|
|
||||||
defer impl_methods.deinit();
|
|
||||||
for (ib.methods) |method_node| {
|
|
||||||
if (method_node.data == .fn_decl) {
|
|
||||||
const method_fd = &method_node.data.fn_decl;
|
|
||||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue;
|
|
||||||
self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
|
||||||
self.program_index.import_flags.put(qualified, is_imported) catch {};
|
|
||||||
self.declareFunction(method_fd, qualified);
|
|
||||||
impl_methods.put(method_fd.name, {}) catch {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Synthesize default methods from protocol declaration
|
|
||||||
if (self.program_index.protocol_ast_map.get(ib.protocol_name)) |pd| {
|
|
||||||
for (pd.methods) |method| {
|
|
||||||
if (method.default_body != null and !impl_methods.contains(method.name)) {
|
|
||||||
// Create a synthesized fn_decl for the default method
|
|
||||||
const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type);
|
|
||||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue;
|
|
||||||
self.program_index.fn_ast_map.put(qualified, synth_fd) catch {};
|
|
||||||
self.program_index.import_flags.put(qualified, is_imported) catch {};
|
|
||||||
self.declareFunction(synth_fd, qualified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register a parameterised-protocol impl into `param_impl_map`.
|
|
||||||
/// Resolves the protocol's type args + the source type, mangles them, and
|
|
||||||
/// stashes the impl's method fn_decls for later monomorphisation by
|
|
||||||
/// `lowerXX`. Same-module duplicate impls produce a diagnostic here;
|
|
||||||
/// cross-module duplicates are detected at the xx resolution site.
|
|
||||||
///
|
|
||||||
/// Pack-shaped sources (`Closure(..$args) -> $R`, detected via
|
|
||||||
/// `pack_start != null`) are additionally registered into
|
|
||||||
/// `param_impl_pack_map` keyed without the source suffix — the matching
|
|
||||||
/// site walks that map to bind packs against any concrete closure shape.
|
|
||||||
fn registerParamImpl(self: *Lowering, ib: *const ast.ImplBlock, decl: *const Node, is_imported: bool) void {
|
|
||||||
const table = &self.module.types;
|
|
||||||
|
|
||||||
// Resolve the protocol's type-arg list to concrete TypeIds.
|
|
||||||
var arg_tys = std.ArrayList(TypeId).empty;
|
|
||||||
for (ib.protocol_type_args) |arg_node| {
|
|
||||||
const t = type_bridge.resolveAstType(arg_node, table, &self.program_index.type_alias_map);
|
|
||||||
arg_tys.append(self.alloc, t) catch return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the source type. Parser stores it on `target_type_expr` for
|
|
||||||
// parameterised impls (back-compat `target_type` string is kept for
|
|
||||||
// simple cases but the canonical form is the TypeExpr).
|
|
||||||
const src_ty: TypeId = if (ib.target_type_expr) |te|
|
|
||||||
type_bridge.resolveAstType(te, table, &self.program_index.type_alias_map)
|
|
||||||
else if (ib.target_type.len > 0)
|
|
||||||
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.program_index.type_alias_map)
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Mangle into the lookup key.
|
|
||||||
var key_buf = std.ArrayList(u8).empty;
|
|
||||||
key_buf.appendSlice(self.alloc, ib.protocol_name) catch return;
|
|
||||||
for (arg_tys.items) |t| {
|
|
||||||
key_buf.append(self.alloc, 0) catch return;
|
|
||||||
key_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return;
|
|
||||||
}
|
|
||||||
const pack_key_len = key_buf.items.len; // proto + args, no src — used for pack map
|
|
||||||
key_buf.append(self.alloc, 0) catch return;
|
|
||||||
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return;
|
|
||||||
const key = key_buf.items;
|
|
||||||
|
|
||||||
// Collect method fn_decl pointers.
|
|
||||||
var methods = std.ArrayList(*const ast.FnDecl).empty;
|
|
||||||
for (ib.methods) |method_node| {
|
|
||||||
if (method_node.data == .fn_decl) {
|
|
||||||
methods.append(self.alloc, &method_node.data.fn_decl) catch {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defining_module: []const u8 = self.current_source_file orelse "";
|
|
||||||
const entry: ParamImplEntry = .{
|
|
||||||
.methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
|
|
||||||
.source_ty = src_ty,
|
|
||||||
.target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return,
|
|
||||||
.defining_module = defining_module,
|
|
||||||
.span = decl.span,
|
|
||||||
};
|
|
||||||
|
|
||||||
const gop = self.param_impl_map.getOrPut(key) catch return;
|
|
||||||
if (!gop.found_existing) {
|
|
||||||
gop.value_ptr.* = std.ArrayList(ParamImplEntry).empty;
|
|
||||||
} else {
|
|
||||||
// Same-file duplicate is an immediate error. Cross-file overlaps
|
|
||||||
// are deferred to the xx resolution site (Phase 5) so the impl
|
|
||||||
// surface can be richer than any one file's view.
|
|
||||||
for (gop.value_ptr.items) |existing| {
|
|
||||||
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
|
|
||||||
if (self.diagnostics) |diags| {
|
|
||||||
diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{
|
|
||||||
ib.protocol_name, self.mangleTypeName(src_ty), defining_module,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gop.value_ptr.append(self.alloc, entry) catch return;
|
|
||||||
|
|
||||||
// Concrete-struct source: also register the impl's methods as
|
|
||||||
// `<Source>.<method>` in fn_ast_map so UFCS resolves them (e.g.
|
|
||||||
// `xs[i].get()` on a pack element). For a concrete impl like
|
|
||||||
// `impl Box(s64) for IntCell`, the method is already fully concrete —
|
|
||||||
// nothing to monomorphize, unlike generic/pack sources (which stay
|
|
||||||
// lazy in param_impl_map and are handled below).
|
|
||||||
{
|
|
||||||
const si = table.get(src_ty);
|
|
||||||
if (!src_ty.isBuiltin() and si == .@"struct") {
|
|
||||||
const src_name = self.formatTypeName(src_ty);
|
|
||||||
// A generic-struct source (`impl VL($R) for Combined($R, ..$Ts)`)
|
|
||||||
// registers each method as a TEMPLATE only: its signature
|
|
||||||
// references unbound type params (`-> $R`), so declaring it as a
|
|
||||||
// standalone function would emit garbage (an unresolved return
|
|
||||||
// type). Concrete instances are monomorphized per-erasure by
|
|
||||||
// createProtocolThunk via this same fn_ast_map entry.
|
|
||||||
const is_generic_src = self.program_index.struct_template_map.contains(src_name);
|
|
||||||
for (methods.items) |mfd| {
|
|
||||||
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue;
|
|
||||||
if (self.program_index.fn_ast_map.contains(q)) continue; // first impl wins
|
|
||||||
self.program_index.fn_ast_map.put(q, mfd) catch {};
|
|
||||||
self.program_index.import_flags.put(q, is_imported) catch {};
|
|
||||||
if (!is_generic_src) self.declareFunction(mfd, q);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pack-shaped source: also register in the pack map. The source
|
|
||||||
// closure carries `pack_start` set; matching binds the source's
|
|
||||||
// tail param types to the pack-name and the source's return to
|
|
||||||
// the impl's return-type-var (when the return is generic).
|
|
||||||
const src_info = table.get(src_ty);
|
|
||||||
if (src_info == .closure and src_info.closure.pack_start != null) {
|
|
||||||
const target_expr_node = ib.target_type_expr orelse return;
|
|
||||||
if (target_expr_node.data != .closure_type_expr) return;
|
|
||||||
const ct = target_expr_node.data.closure_type_expr;
|
|
||||||
const pack_var = ct.pack_name orelse return;
|
|
||||||
// Extract the return-type-var name if the impl's return is generic.
|
|
||||||
// `Closure(...) -> $R` parses with the return-type node carrying
|
|
||||||
// `is_generic = true`. Concrete returns leave it null.
|
|
||||||
var ret_var: ?[]const u8 = null;
|
|
||||||
if (ct.return_type) |rt| {
|
|
||||||
if (rt.data == .type_expr and rt.data.type_expr.is_generic) {
|
|
||||||
ret_var = rt.data.type_expr.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const pack_entry: PackParamImplEntry = .{
|
|
||||||
.methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
|
|
||||||
.source_pack_ty = src_ty,
|
|
||||||
.target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return,
|
|
||||||
.defining_module = defining_module,
|
|
||||||
.span = decl.span,
|
|
||||||
.pack_var_name = self.alloc.dupe(u8, pack_var) catch return,
|
|
||||||
.ret_var_name = if (ret_var) |rv| (self.alloc.dupe(u8, rv) catch return) else null,
|
|
||||||
};
|
|
||||||
const pack_key = key_buf.items[0..pack_key_len];
|
|
||||||
const pack_key_owned = self.alloc.dupe(u8, pack_key) catch return;
|
|
||||||
const pgop = self.param_impl_pack_map.getOrPut(pack_key_owned) catch return;
|
|
||||||
if (!pgop.found_existing) {
|
|
||||||
pgop.value_ptr.* = std.ArrayList(PackParamImplEntry).empty;
|
|
||||||
} else {
|
|
||||||
for (pgop.value_ptr.items) |existing| {
|
|
||||||
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
|
|
||||||
if (self.diagnostics) |diags| {
|
|
||||||
diags.addFmt(.err, decl.span, "duplicate pack impl '{s}' for source '{s}' in {s}", .{
|
|
||||||
ib.protocol_name, self.mangleTypeName(src_ty), defining_module,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pgop.value_ptr.append(self.alloc, pack_entry) catch return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synthesize a fn_decl from a protocol default method for a concrete type.
|
|
||||||
fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl {
|
|
||||||
// Build parameter list: self: *TargetType, then the protocol method params
|
|
||||||
var params_list = std.ArrayList(ast.Param).empty;
|
|
||||||
defer params_list.deinit(self.alloc);
|
|
||||||
|
|
||||||
// Add self parameter: self: *TargetType
|
|
||||||
const self_type_node = self.alloc.create(ast.Node) catch unreachable;
|
|
||||||
const pointee_node = self.alloc.create(ast.Node) catch unreachable;
|
|
||||||
pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } };
|
|
||||||
self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{
|
|
||||||
.pointee_type = pointee_node,
|
|
||||||
} } };
|
|
||||||
params_list.append(self.alloc, .{
|
|
||||||
.name = "self",
|
|
||||||
.name_span = .{ .start = 0, .end = 0 },
|
|
||||||
.type_expr = self_type_node,
|
|
||||||
}) catch unreachable;
|
|
||||||
|
|
||||||
// Add remaining params from the protocol method
|
|
||||||
for (method.params, method.param_names) |pty, pname| {
|
|
||||||
params_list.append(self.alloc, .{
|
|
||||||
.name = pname,
|
|
||||||
.name_span = .{ .start = 0, .end = 0 },
|
|
||||||
.type_expr = pty,
|
|
||||||
}) catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fd = self.alloc.create(ast.FnDecl) catch unreachable;
|
|
||||||
fd.* = .{
|
|
||||||
.name = method.name,
|
|
||||||
.params = self.alloc.dupe(ast.Param, params_list.items) catch unreachable,
|
|
||||||
.body = method.default_body.?,
|
|
||||||
.return_type = method.return_type,
|
|
||||||
};
|
|
||||||
return fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Protocol dispatch ──────────────────────────────────────────
|
// ── Protocol dispatch ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const ast = @import("../ast.zig");
|
const ast = @import("../ast.zig");
|
||||||
const Node = ast.Node;
|
const Node = ast.Node;
|
||||||
|
const errors = @import("../errors.zig");
|
||||||
|
|
||||||
const ir_mod = @import("ir.zig");
|
const ir_mod = @import("ir.zig");
|
||||||
const TypeId = ir_mod.TypeId;
|
const TypeId = ir_mod.TypeId;
|
||||||
@@ -19,6 +20,18 @@ fn protoMethodReq(name: []const u8) ast.ProtocolMethodDecl {
|
|||||||
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
|
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mk(alloc: std.mem.Allocator, data: ast.Node.Data) *Node {
|
||||||
|
const n = alloc.create(Node) catch unreachable;
|
||||||
|
n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data };
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
fn typeExpr(alloc: std.mem.Allocator, name: []const u8) *Node {
|
||||||
|
return mk(alloc, .{ .type_expr = .{ .name = name, .is_generic = false } });
|
||||||
|
}
|
||||||
|
fn emptyBody(alloc: std.mem.Allocator) *Node {
|
||||||
|
return mk(alloc, .{ .block = .{ .stmts = &.{} } });
|
||||||
|
}
|
||||||
|
|
||||||
test "protocols: getProtocolInfo resolves registered protocol structs only" {
|
test "protocols: getProtocolInfo resolves registered protocol structs only" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
@@ -105,3 +118,69 @@ test "protocols: packArgConformsTo at the impl-declaration level (non-parameteri
|
|||||||
// An unregistered protocol name conforms to nothing.
|
// An unregistered protocol name conforms to nothing.
|
||||||
try std.testing.expect(!pr.packArgConformsTo("Nope", circle));
|
try std.testing.expect(!pr.packArgConformsTo("Nope", circle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "protocols: registerImplBlock records <Target>.<method> in fn_ast_map" {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
var module = ir_mod.Module.init(alloc);
|
||||||
|
defer module.deinit();
|
||||||
|
var l = Lowering.init(&module);
|
||||||
|
const pr = ProtocolResolver{ .l = &l };
|
||||||
|
|
||||||
|
// Drawable :: protocol { draw :: (); } + impl Drawable for Circle { draw :: (){} }
|
||||||
|
const proto_methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
|
||||||
|
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &proto_methods };
|
||||||
|
l.registerProtocolDecl(&pd);
|
||||||
|
|
||||||
|
const draw_node = mk(alloc, .{ .fn_decl = .{ .name = "draw", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
|
||||||
|
const methods = [_]*Node{draw_node};
|
||||||
|
const ib = ast.ImplBlock{ .protocol_name = "Drawable", .target_type = "Circle", .methods = &methods };
|
||||||
|
const decl = mk(alloc, .{ .impl_block = ib });
|
||||||
|
|
||||||
|
// Not registered before; the non-parameterised impl registers `Circle.draw`.
|
||||||
|
try std.testing.expect(!l.program_index.fn_ast_map.contains("Circle.draw"));
|
||||||
|
pr.registerImplBlock(&ib, false, decl);
|
||||||
|
try std.testing.expect(l.program_index.fn_ast_map.contains("Circle.draw"));
|
||||||
|
// And it now conforms (same fn_ast_map entry packArgConformsTo checks).
|
||||||
|
const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
|
||||||
|
try std.testing.expect(pr.packArgConformsTo("Drawable", circle));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "protocols: registerParamImpl flags a same-file duplicate impl" {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
var module = ir_mod.Module.init(alloc);
|
||||||
|
defer module.deinit();
|
||||||
|
var diags = errors.DiagnosticList.init(alloc, "", "test.sx");
|
||||||
|
defer diags.deinit();
|
||||||
|
var l = Lowering.init(&module);
|
||||||
|
l.diagnostics = &diags;
|
||||||
|
l.current_source_file = "test.sx"; // both impls share a defining module
|
||||||
|
|
||||||
|
const pr = ProtocolResolver{ .l = &l };
|
||||||
|
_ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("IntCell"), .fields = &.{} } });
|
||||||
|
|
||||||
|
// impl Into(s64) for IntCell { ... } — a parameterised-protocol impl.
|
||||||
|
const args = [_]*Node{typeExpr(alloc, "s64")};
|
||||||
|
const conv = mk(alloc, .{ .fn_decl = .{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
|
||||||
|
const methods = [_]*Node{conv};
|
||||||
|
const ib = ast.ImplBlock{
|
||||||
|
.protocol_name = "Into",
|
||||||
|
.target_type = "IntCell",
|
||||||
|
.methods = &methods,
|
||||||
|
.protocol_type_args = &args,
|
||||||
|
};
|
||||||
|
const decl = mk(alloc, .{ .impl_block = ib });
|
||||||
|
|
||||||
|
// First registration is fine; the second (same key, same module) is a dup.
|
||||||
|
pr.registerImplBlock(&ib, false, decl);
|
||||||
|
pr.registerImplBlock(&ib, false, decl);
|
||||||
|
|
||||||
|
var dup_reported = false;
|
||||||
|
for (diags.items.items) |d| {
|
||||||
|
if (d.level == .err and std.mem.indexOf(u8, d.message, "duplicate impl 'Into'") != null) dup_reported = true;
|
||||||
|
}
|
||||||
|
try std.testing.expect(dup_reported);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,36 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const ast = @import("../ast.zig");
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
|
const type_bridge = @import("type_bridge.zig");
|
||||||
const lower = @import("lower.zig");
|
const lower = @import("lower.zig");
|
||||||
const program_index_mod = @import("program_index.zig");
|
const program_index_mod = @import("program_index.zig");
|
||||||
|
|
||||||
|
const Node = ast.Node;
|
||||||
const TypeId = types.TypeId;
|
const TypeId = types.TypeId;
|
||||||
const Lowering = lower.Lowering;
|
const Lowering = lower.Lowering;
|
||||||
const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo;
|
const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo;
|
||||||
|
const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo;
|
||||||
|
|
||||||
/// Protocol / impl LOOKUP (architecture phase A4.2, first increment), extracted
|
/// Protocol / impl LOOKUP + REGISTRATION (architecture phase A4.2), extracted
|
||||||
/// from `Lowering`. Owns the read-only conformance queries:
|
/// from `Lowering`. Owns:
|
||||||
/// - `getProtocolInfo` — is a type a registered protocol, and its method table,
|
/// - read-only conformance queries: `getProtocolInfo` (is a type a registered
|
||||||
/// - `hasImplPlain` — has a (protocol, type) pair had its thunks materialized,
|
/// protocol + its method table), `hasImplPlain` (have a (protocol, type)
|
||||||
/// - `packArgConformsTo` — does a type conform to a protocol at the
|
/// pair's thunks been materialized), `packArgConformsTo` (impl-declaration
|
||||||
/// impl-declaration level (for protocol-pack `..xs: P` elements).
|
/// conformance for protocol-pack `..xs: P` elements),
|
||||||
|
/// - registration: `registerProtocolDecl` (protocol struct + method table +
|
||||||
|
/// vtable type), `registerImplBlock` / `registerParamImpl` (populate the
|
||||||
|
/// impl maps + the `0410`/`0411`/`0412` visibility/duplicate diagnostics),
|
||||||
|
/// and the default-method synthesis they use.
|
||||||
///
|
///
|
||||||
/// A `*Lowering` facade (Principle 5, like `GenericResolver` / `CallResolver`):
|
/// A `*Lowering` facade (Principle 5, like `GenericResolver` / `CallResolver`):
|
||||||
/// these read the protocol/impl registries (`protocol_decl_map` /
|
/// it reads/writes the protocol/impl registries (`protocol_decl_map` /
|
||||||
/// `protocol_ast_map` in `ProgramIndex`; `protocol_thunk_map` / `param_impl_map`
|
/// `protocol_ast_map` in `ProgramIndex`; `protocol_thunk_map` / `param_impl_map`
|
||||||
/// on `Lowering`) plus the type table, so it borrows `*Lowering` rather than
|
/// / `param_impl_pack_map` / `protocol_vtable_type_map` on `Lowering`) plus the
|
||||||
/// re-threading every map. Registration (`register*`) and IR emission
|
/// type table, so it borrows `*Lowering` rather than re-threading every map.
|
||||||
/// (`createProtocolThunk` / `buildProtocolValue` / `tryUserConversion`) stay in
|
/// IR EMISSION stays in `Lowering` for the later A4.2 increment — registration
|
||||||
/// `Lowering` for the later A4.2 increments — this step moves only pure lookup.
|
/// calls `self.l.declareFunction` (the emission primitive) but the thunk/value
|
||||||
|
/// builders (`createProtocolThunk` / `buildProtocolValue` / `tryUserConversion`)
|
||||||
|
/// are NOT moved here.
|
||||||
pub const ProtocolResolver = struct {
|
pub const ProtocolResolver = struct {
|
||||||
l: *Lowering,
|
l: *Lowering,
|
||||||
|
|
||||||
@@ -82,4 +92,349 @@ pub const ProtocolResolver = struct {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Registration ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn registerProtocolDecl(self: ProtocolResolver, pd: *const ast.ProtocolDecl) void {
|
||||||
|
// Decision 4 soft-convention warning: a type-arg and a method (the
|
||||||
|
// "runtime accessor" namespace — protocols have no fields) sharing a
|
||||||
|
// name is allowed, but `..pack.<name>` then resolves by *position*
|
||||||
|
// rather than by precedence, which surprises readers. Alert at decl.
|
||||||
|
for (pd.type_params) |tp| {
|
||||||
|
for (pd.methods) |m| {
|
||||||
|
if (std.mem.eql(u8, tp.name, m.name)) {
|
||||||
|
if (self.l.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.warn, null, "protocol '{s}' declares type-arg and method both named '{s}'; `..pack.{s}` resolves by position (type-arg in type position, method in value position)", .{ pd.name, tp.name, tp.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameterised protocols are compile-time-only — no vtable, no boxed
|
||||||
|
// instance struct. Methods reference unbound type params (e.g.
|
||||||
|
// `convert :: () -> Target`) that only get a concrete TypeId per
|
||||||
|
// (Source, Target) pair at xx resolution time. Stash the AST so
|
||||||
|
// `param_impl_map` lookup can resolve method signatures lazily.
|
||||||
|
if (pd.type_params.len > 0) {
|
||||||
|
self.l.program_index.protocol_ast_map.put(pd.name, pd) catch {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = &self.l.module.types;
|
||||||
|
const name_id = table.internString(pd.name);
|
||||||
|
|
||||||
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
||||||
|
|
||||||
|
// First field: ctx: *void
|
||||||
|
const void_ptr_ty = table.ptrTo(.void);
|
||||||
|
fields.append(self.l.alloc, .{
|
||||||
|
.name = table.internString("ctx"),
|
||||||
|
.ty = void_ptr_ty,
|
||||||
|
}) catch unreachable;
|
||||||
|
|
||||||
|
if (pd.is_inline) {
|
||||||
|
// One fn-ptr field per protocol method
|
||||||
|
for (pd.methods) |method| {
|
||||||
|
fields.append(self.l.alloc, .{
|
||||||
|
.name = table.internString(method.name),
|
||||||
|
.ty = void_ptr_ty, // fn ptrs are opaque pointers
|
||||||
|
}) catch unreachable;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vtable pointer
|
||||||
|
fields.append(self.l.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);
|
||||||
|
|
||||||
|
// Build protocol method info for dispatch
|
||||||
|
var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
|
||||||
|
for (pd.methods) |method| {
|
||||||
|
var ptypes = std.ArrayList(TypeId).empty;
|
||||||
|
for (method.params) |p| {
|
||||||
|
// Self → *void for protocol context; everything else
|
||||||
|
// goes through `resolveAstType`, threaded with the canonical
|
||||||
|
// alias map (`ProgramIndex.type_alias_map`).
|
||||||
|
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 type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map);
|
||||||
|
};
|
||||||
|
ptypes.append(self.l.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 type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map);
|
||||||
|
} else .void;
|
||||||
|
method_infos.append(self.l.alloc, .{
|
||||||
|
.name = method.name,
|
||||||
|
.param_types = self.l.alloc.dupe(TypeId, ptypes.items) catch unreachable,
|
||||||
|
.ret_type = ret,
|
||||||
|
.ret_is_self = ret_is_self,
|
||||||
|
}) catch unreachable;
|
||||||
|
}
|
||||||
|
self.l.program_index.protocol_decl_map.put(pd.name, .{
|
||||||
|
.name = pd.name,
|
||||||
|
.is_inline = pd.is_inline,
|
||||||
|
.methods = self.l.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable,
|
||||||
|
}) catch {};
|
||||||
|
self.l.program_index.protocol_ast_map.put(pd.name, pd) catch {};
|
||||||
|
|
||||||
|
// For vtable protocols, create the vtable struct type
|
||||||
|
if (!pd.is_inline) {
|
||||||
|
var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
||||||
|
for (pd.methods) |method| {
|
||||||
|
vtable_fields.append(self.l.alloc, .{
|
||||||
|
.name = table.internString(method.name),
|
||||||
|
.ty = void_ptr_ty,
|
||||||
|
}) catch unreachable;
|
||||||
|
}
|
||||||
|
var vtable_name_buf: [128]u8 = undefined;
|
||||||
|
const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable";
|
||||||
|
const vtable_name_id = table.internString(vtable_name);
|
||||||
|
const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } };
|
||||||
|
const vtable_ty = table.intern(vtable_info);
|
||||||
|
self.l.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registerImplBlock(self: ProtocolResolver, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void {
|
||||||
|
// Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`):
|
||||||
|
// record into `param_impl_map` for compile-time resolution by `lowerXX`.
|
||||||
|
// Methods are NOT registered in fn_ast_map — they're monomorphised lazily
|
||||||
|
// per (Source, Target) pair at the xx call site.
|
||||||
|
if (ib.protocol_type_args.len > 0) {
|
||||||
|
self.registerParamImpl(ib, decl, is_imported);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Collect explicitly implemented method names
|
||||||
|
var impl_methods = std.StringHashMap(void).init(self.l.alloc);
|
||||||
|
defer impl_methods.deinit();
|
||||||
|
for (ib.methods) |method_node| {
|
||||||
|
if (method_node.data == .fn_decl) {
|
||||||
|
const method_fd = &method_node.data.fn_decl;
|
||||||
|
const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue;
|
||||||
|
self.l.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
||||||
|
self.l.program_index.import_flags.put(qualified, is_imported) catch {};
|
||||||
|
self.l.declareFunction(method_fd, qualified);
|
||||||
|
impl_methods.put(method_fd.name, {}) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Synthesize default methods from protocol declaration
|
||||||
|
if (self.l.program_index.protocol_ast_map.get(ib.protocol_name)) |pd| {
|
||||||
|
for (pd.methods) |method| {
|
||||||
|
if (method.default_body != null and !impl_methods.contains(method.name)) {
|
||||||
|
// Create a synthesized fn_decl for the default method
|
||||||
|
const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type);
|
||||||
|
const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue;
|
||||||
|
self.l.program_index.fn_ast_map.put(qualified, synth_fd) catch {};
|
||||||
|
self.l.program_index.import_flags.put(qualified, is_imported) catch {};
|
||||||
|
self.l.declareFunction(synth_fd, qualified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a parameterised-protocol impl into `param_impl_map`.
|
||||||
|
/// Resolves the protocol's type args + the source type, mangles them, and
|
||||||
|
/// stashes the impl's method fn_decls for later monomorphisation by
|
||||||
|
/// `lowerXX`. Same-module duplicate impls produce a diagnostic here;
|
||||||
|
/// cross-module duplicates are detected at the xx resolution site.
|
||||||
|
///
|
||||||
|
/// Pack-shaped sources (`Closure(..$args) -> $R`, detected via
|
||||||
|
/// `pack_start != null`) are additionally registered into
|
||||||
|
/// `param_impl_pack_map` keyed without the source suffix — the matching
|
||||||
|
/// site walks that map to bind packs against any concrete closure shape.
|
||||||
|
pub fn registerParamImpl(self: ProtocolResolver, ib: *const ast.ImplBlock, decl: *const Node, is_imported: bool) void {
|
||||||
|
const table = &self.l.module.types;
|
||||||
|
|
||||||
|
// Resolve the protocol's type-arg list to concrete TypeIds.
|
||||||
|
var arg_tys = std.ArrayList(TypeId).empty;
|
||||||
|
for (ib.protocol_type_args) |arg_node| {
|
||||||
|
const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map);
|
||||||
|
arg_tys.append(self.l.alloc, t) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the source type. Parser stores it on `target_type_expr` for
|
||||||
|
// parameterised impls (back-compat `target_type` string is kept for
|
||||||
|
// simple cases but the canonical form is the TypeExpr).
|
||||||
|
const src_ty: TypeId = if (ib.target_type_expr) |te|
|
||||||
|
type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map)
|
||||||
|
else if (ib.target_type.len > 0)
|
||||||
|
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map)
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Mangle into the lookup key.
|
||||||
|
var key_buf = std.ArrayList(u8).empty;
|
||||||
|
key_buf.appendSlice(self.l.alloc, ib.protocol_name) catch return;
|
||||||
|
for (arg_tys.items) |t| {
|
||||||
|
key_buf.append(self.l.alloc, 0) catch return;
|
||||||
|
key_buf.appendSlice(self.l.alloc, self.l.mangleTypeName(t)) catch return;
|
||||||
|
}
|
||||||
|
const pack_key_len = key_buf.items.len; // proto + args, no src — used for pack map
|
||||||
|
key_buf.append(self.l.alloc, 0) catch return;
|
||||||
|
key_buf.appendSlice(self.l.alloc, self.l.mangleTypeName(src_ty)) catch return;
|
||||||
|
const key = key_buf.items;
|
||||||
|
|
||||||
|
// Collect method fn_decl pointers.
|
||||||
|
var methods = std.ArrayList(*const ast.FnDecl).empty;
|
||||||
|
for (ib.methods) |method_node| {
|
||||||
|
if (method_node.data == .fn_decl) {
|
||||||
|
methods.append(self.l.alloc, &method_node.data.fn_decl) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defining_module: []const u8 = self.l.current_source_file orelse "";
|
||||||
|
const entry: Lowering.ParamImplEntry = .{
|
||||||
|
.methods = self.l.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
|
||||||
|
.source_ty = src_ty,
|
||||||
|
.target_args = self.l.alloc.dupe(TypeId, arg_tys.items) catch return,
|
||||||
|
.defining_module = defining_module,
|
||||||
|
.span = decl.span,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gop = self.l.param_impl_map.getOrPut(key) catch return;
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = std.ArrayList(Lowering.ParamImplEntry).empty;
|
||||||
|
} else {
|
||||||
|
// Same-file duplicate is an immediate error. Cross-file overlaps
|
||||||
|
// are deferred to the xx resolution site (Phase 5) so the impl
|
||||||
|
// surface can be richer than any one file's view.
|
||||||
|
for (gop.value_ptr.items) |existing| {
|
||||||
|
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
|
||||||
|
if (self.l.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{
|
||||||
|
ib.protocol_name, self.l.mangleTypeName(src_ty), defining_module,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gop.value_ptr.append(self.l.alloc, entry) catch return;
|
||||||
|
|
||||||
|
// Concrete-struct source: also register the impl's methods as
|
||||||
|
// `<Source>.<method>` in fn_ast_map so UFCS resolves them (e.g.
|
||||||
|
// `xs[i].get()` on a pack element). For a concrete impl like
|
||||||
|
// `impl Box(s64) for IntCell`, the method is already fully concrete —
|
||||||
|
// nothing to monomorphize, unlike generic/pack sources (which stay
|
||||||
|
// lazy in param_impl_map and are handled below).
|
||||||
|
{
|
||||||
|
const si = table.get(src_ty);
|
||||||
|
if (!src_ty.isBuiltin() and si == .@"struct") {
|
||||||
|
const src_name = self.l.formatTypeName(src_ty);
|
||||||
|
// A generic-struct source (`impl VL($R) for Combined($R, ..$Ts)`)
|
||||||
|
// registers each method as a TEMPLATE only: its signature
|
||||||
|
// references unbound type params (`-> $R`), so declaring it as a
|
||||||
|
// standalone function would emit garbage (an unresolved return
|
||||||
|
// type). Concrete instances are monomorphized per-erasure by
|
||||||
|
// createProtocolThunk via this same fn_ast_map entry.
|
||||||
|
const is_generic_src = self.l.program_index.struct_template_map.contains(src_name);
|
||||||
|
for (methods.items) |mfd| {
|
||||||
|
const q = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue;
|
||||||
|
if (self.l.program_index.fn_ast_map.contains(q)) continue; // first impl wins
|
||||||
|
self.l.program_index.fn_ast_map.put(q, mfd) catch {};
|
||||||
|
self.l.program_index.import_flags.put(q, is_imported) catch {};
|
||||||
|
if (!is_generic_src) self.l.declareFunction(mfd, q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack-shaped source: also register in the pack map. The source
|
||||||
|
// closure carries `pack_start` set; matching binds the source's
|
||||||
|
// tail param types to the pack-name and the source's return to
|
||||||
|
// the impl's return-type-var (when the return is generic).
|
||||||
|
const src_info = table.get(src_ty);
|
||||||
|
if (src_info == .closure and src_info.closure.pack_start != null) {
|
||||||
|
const target_expr_node = ib.target_type_expr orelse return;
|
||||||
|
if (target_expr_node.data != .closure_type_expr) return;
|
||||||
|
const ct = target_expr_node.data.closure_type_expr;
|
||||||
|
const pack_var = ct.pack_name orelse return;
|
||||||
|
// Extract the return-type-var name if the impl's return is generic.
|
||||||
|
// `Closure(...) -> $R` parses with the return-type node carrying
|
||||||
|
// `is_generic = true`. Concrete returns leave it null.
|
||||||
|
var ret_var: ?[]const u8 = null;
|
||||||
|
if (ct.return_type) |rt| {
|
||||||
|
if (rt.data == .type_expr and rt.data.type_expr.is_generic) {
|
||||||
|
ret_var = rt.data.type_expr.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pack_entry: Lowering.PackParamImplEntry = .{
|
||||||
|
.methods = self.l.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
|
||||||
|
.source_pack_ty = src_ty,
|
||||||
|
.target_args = self.l.alloc.dupe(TypeId, arg_tys.items) catch return,
|
||||||
|
.defining_module = defining_module,
|
||||||
|
.span = decl.span,
|
||||||
|
.pack_var_name = self.l.alloc.dupe(u8, pack_var) catch return,
|
||||||
|
.ret_var_name = if (ret_var) |rv| (self.l.alloc.dupe(u8, rv) catch return) else null,
|
||||||
|
};
|
||||||
|
const pack_key = key_buf.items[0..pack_key_len];
|
||||||
|
const pack_key_owned = self.l.alloc.dupe(u8, pack_key) catch return;
|
||||||
|
const pgop = self.l.param_impl_pack_map.getOrPut(pack_key_owned) catch return;
|
||||||
|
if (!pgop.found_existing) {
|
||||||
|
pgop.value_ptr.* = std.ArrayList(Lowering.PackParamImplEntry).empty;
|
||||||
|
} else {
|
||||||
|
for (pgop.value_ptr.items) |existing| {
|
||||||
|
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
|
||||||
|
if (self.l.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.err, decl.span, "duplicate pack impl '{s}' for source '{s}' in {s}", .{
|
||||||
|
ib.protocol_name, self.l.mangleTypeName(src_ty), defining_module,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pgop.value_ptr.append(self.l.alloc, pack_entry) catch return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthesize a fn_decl from a protocol default method for a concrete type.
|
||||||
|
fn synthesizeDefaultMethod(self: ProtocolResolver, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl {
|
||||||
|
// Build parameter list: self: *TargetType, then the protocol method params
|
||||||
|
var params_list = std.ArrayList(ast.Param).empty;
|
||||||
|
defer params_list.deinit(self.l.alloc);
|
||||||
|
|
||||||
|
// Add self parameter: self: *TargetType
|
||||||
|
const self_type_node = self.l.alloc.create(ast.Node) catch unreachable;
|
||||||
|
const pointee_node = self.l.alloc.create(ast.Node) catch unreachable;
|
||||||
|
pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } };
|
||||||
|
self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{
|
||||||
|
.pointee_type = pointee_node,
|
||||||
|
} } };
|
||||||
|
params_list.append(self.l.alloc, .{
|
||||||
|
.name = "self",
|
||||||
|
.name_span = .{ .start = 0, .end = 0 },
|
||||||
|
.type_expr = self_type_node,
|
||||||
|
}) catch unreachable;
|
||||||
|
|
||||||
|
// Add remaining params from the protocol method
|
||||||
|
for (method.params, method.param_names) |pty, pname| {
|
||||||
|
params_list.append(self.l.alloc, .{
|
||||||
|
.name = pname,
|
||||||
|
.name_span = .{ .start = 0, .end = 0 },
|
||||||
|
.type_expr = pty,
|
||||||
|
}) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = self.l.alloc.create(ast.FnDecl) catch unreachable;
|
||||||
|
fd.* = .{
|
||||||
|
.name = method.name,
|
||||||
|
.params = self.l.alloc.dupe(ast.Param, params_list.items) catch unreachable,
|
||||||
|
.body = method.default_body.?,
|
||||||
|
.return_type = method.return_type,
|
||||||
|
};
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user