ffi M5.A.next.1d.B: pack impl matching — bind $args + $R per call
Pack-shaped impls (`impl P(...) for Closure(..$args) -> $R`) now match concrete closure sources at xx resolution time. Concrete impls keep their priority — pack matching only fires on a concrete-key miss in `param_impl_map`. New plumbing in src/ir/lower.zig: - `PackParamImplEntry` carries the pack-shaped source TypeId plus the pack-var and ret-var names extracted from the impl AST's `target_type_expr`. `registerParamImpl` detects pack-shaped sources via `pack_start != null` on the resolved closure type and additionally registers in a new `param_impl_pack_map` keyed by `"Proto\x00<arg_mangled>"` (no source suffix). - `tryUserConversion` re-shapes the concrete lookup so the pack path runs on miss. `tryPackImplMatch` walks the pack entries, verifies the source's fixed prefix matches the impl's prefix, binds the pack-var to the source's tail param TypeIds, binds the ret-var (when the impl's return is generic) to the source return, and monomorphises the convert method. Mangled name stays keyed on the concrete source so distinct call shapes monomorphise separately. - `pack_bindings: ?StringHashMap([]const TypeId)` is saved/ restored around monomorphisation, mirroring `type_bindings`. - `resolveClosureTypeWithBindings` handles the closure_type_expr node during type resolution: when the closure carries a `pack_name` AND `pack_bindings` has a binding for it, the bound TypeIds are appended after the fixed prefix and the result is a concrete (non-pack) closure type — so the impl body's `self: Closure(..$args) -> $R` substitutes to the concrete source closure during monomorphisation. Without an active binding, the pack shape is preserved. `examples/155-pack-impl-match.sx` flips from the "no Into(Block) for cl_s32_bool__bool" lock-in diagnostic to "pack impl match ok": one user-declared `impl Into(Block) for Closure(..$args) -> $R` covers a `Closure(s32, bool) -> bool` source that stdlib has no hand-rolled impl for. Constructed Block isn't invoked (invoke=null) — the test exercises only the matching + monomorphisation, not the trampoline (step 5 of the plan). Existing concrete-impl paths unchanged: 95-objc-block-noop, 96-objc-block-multi-arg, and stdlib's hand-rolled `Into(Block) for Closure(bool) -> void` continue to pass through the concrete map first. Same-file duplicate pack impls diagnose at registration; cross-module visibility and multi-pack-impl specificity stay TODOs (matching the deferred Phase 5 work on the concrete path). 193/193 example tests + `zig build test` green.
This commit is contained in:
273
src/ir/lower.zig
273
src/ir/lower.zig
@@ -136,6 +136,16 @@ pub const Lowering = struct {
|
||||
protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId
|
||||
protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId
|
||||
param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00<arg_mangled>\x00<src_mangled>" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap)
|
||||
/// Pack-variadic impl entries — separate map keyed by `"Proto\x00<arg_mangled>"`
|
||||
/// (NO source suffix) so a single impl `Closure(..$args) -> $R` can be
|
||||
/// matched against many concrete source shapes. Concrete impls in
|
||||
/// `param_impl_map` win when both match (specificity rule).
|
||||
param_impl_pack_map: std.StringHashMap(std.ArrayList(PackParamImplEntry)) = std.StringHashMap(std.ArrayList(PackParamImplEntry)).init(std.heap.page_allocator),
|
||||
/// Active pack bindings during monomorphisation. Mirrors `type_bindings`
|
||||
/// but for variadic pack names: `args → [T1, T2, ...]`. Read by
|
||||
/// `resolveTypeWithBindings` on closure_type_expr to substitute
|
||||
/// `Closure(..$args) -> $R` into a concrete closure type.
|
||||
pack_bindings: ?std.StringHashMap([]const TypeId) = 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
|
||||
@@ -188,6 +198,22 @@ pub const Lowering = struct {
|
||||
span: ast.Span,
|
||||
};
|
||||
|
||||
/// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`.
|
||||
/// Matches any concrete closure source whose first `prefix_len` param types
|
||||
/// equal `source_pack_ty`'s fixed prefix; the tail binds to `pack_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`
|
||||
/// means the return type is concrete and must match exactly.
|
||||
const PackParamImplEntry = struct {
|
||||
methods: []const *const ast.FnDecl,
|
||||
source_pack_ty: TypeId,
|
||||
target_args: []const TypeId,
|
||||
defining_module: []const u8,
|
||||
span: ast.Span,
|
||||
pack_var_name: []const u8,
|
||||
ret_var_name: ?[]const u8,
|
||||
};
|
||||
|
||||
/// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports)
|
||||
const StructTemplate = struct {
|
||||
name: []const u8,
|
||||
@@ -9043,6 +9069,9 @@ pub const Lowering = struct {
|
||||
// Handle List(T), Vector(N, T) etc. as type constructor calls
|
||||
return self.resolveTypeCallWithBindings(&cl);
|
||||
},
|
||||
.closure_type_expr => |ct| {
|
||||
return self.resolveClosureTypeWithBindings(&ct);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@@ -9077,6 +9106,9 @@ pub const Lowering = struct {
|
||||
const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0;
|
||||
return self.module.types.arrayOf(elem, len);
|
||||
},
|
||||
.closure_type_expr => |ct| {
|
||||
return self.resolveClosureTypeWithBindings(&ct);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
// Check type aliases before falling through to type_bridge
|
||||
@@ -9086,6 +9118,36 @@ pub const Lowering = struct {
|
||||
return type_bridge.resolveAstType(node, &self.module.types);
|
||||
}
|
||||
|
||||
/// Resolve a `Closure(...)` type expression with the active type/pack
|
||||
/// bindings applied. Pack-shaped closure exprs (`Closure(Prefix..., ..$pack)`)
|
||||
/// substitute `pack` from `self.pack_bindings`, producing a concrete
|
||||
/// closure type — used when monomorphising a pack-variadic impl body
|
||||
/// against a concrete source signature.
|
||||
fn resolveClosureTypeWithBindings(self: *Lowering, ct: *const ast.ClosureTypeExpr) TypeId {
|
||||
var param_ids = std.ArrayList(TypeId).empty;
|
||||
defer param_ids.deinit(self.alloc);
|
||||
for (ct.param_types) |pt| {
|
||||
param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void;
|
||||
}
|
||||
if (ct.pack_name) |pn| {
|
||||
if (self.pack_bindings) |pb| {
|
||||
if (pb.get(pn)) |pack_tys| {
|
||||
for (pack_tys) |t| param_ids.append(self.alloc, t) catch return .void;
|
||||
// Fully bound — emit a concrete closure type, no pack_start.
|
||||
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
|
||||
return self.module.types.closureType(param_ids.items, ret_ty);
|
||||
}
|
||||
}
|
||||
// Pack name in scope but no binding — preserve the pack-shape
|
||||
// so downstream code can still see it's variadic. (Hit during
|
||||
// impl-block parsing before any concrete monomorphisation.)
|
||||
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
|
||||
return self.module.types.closureTypePack(param_ids.items, ret_ty, @intCast(param_ids.items.len));
|
||||
}
|
||||
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
|
||||
return self.module.types.closureType(param_ids.items, ret_ty);
|
||||
}
|
||||
|
||||
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
|
||||
fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
|
||||
const callee_name: []const u8 = switch (cl.callee.data) {
|
||||
@@ -10050,6 +10112,11 @@ pub const Lowering = struct {
|
||||
/// 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) void {
|
||||
const table = &self.module.types;
|
||||
|
||||
@@ -10077,6 +10144,7 @@ pub const Lowering = struct {
|
||||
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;
|
||||
@@ -10117,6 +10185,54 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
gop.value_ptr.append(self.alloc, entry) catch return;
|
||||
|
||||
// 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.
|
||||
@@ -11069,6 +11185,133 @@ pub const Lowering = struct {
|
||||
return dst_info.@"struct".name == block_name;
|
||||
}
|
||||
|
||||
/// Pack-variadic impl matching. Walks `param_impl_pack_map[pack_key]`
|
||||
/// and returns a call ref when a single pack impl matches `src_ty`'s
|
||||
/// shape (concrete src closure / fn with the same fixed prefix as
|
||||
/// the impl's source pack closure). Binds the pack-var to the source's
|
||||
/// tail param types and the return-var (when generic) to the source's
|
||||
/// return type, then monomorphises the convert method.
|
||||
/// Returns null if no pack impls registered for this (proto, dst) or
|
||||
/// none of them match `src_ty`'s shape.
|
||||
fn tryPackImplMatch(
|
||||
self: *Lowering,
|
||||
operand: Ref,
|
||||
operand_node: *const Node,
|
||||
src_ty: TypeId,
|
||||
dst_ty: TypeId,
|
||||
proto_name: []const u8,
|
||||
pack_key: []const u8,
|
||||
guard_key: u64,
|
||||
) ?Ref {
|
||||
_ = operand_node;
|
||||
const pack_entries = self.param_impl_pack_map.get(pack_key) orelse return null;
|
||||
if (pack_entries.items.len == 0) return null;
|
||||
const table = &self.module.types;
|
||||
// Source must itself be a closure/function the pack can match.
|
||||
const src_info = table.get(src_ty);
|
||||
if (src_info != .closure and src_info != .function) return null;
|
||||
|
||||
const src_params: []const TypeId = switch (src_info) {
|
||||
.closure => |c| c.params,
|
||||
.function => |f| f.params,
|
||||
else => unreachable,
|
||||
};
|
||||
const src_ret: TypeId = switch (src_info) {
|
||||
.closure => |c| c.ret,
|
||||
.function => |f| f.ret,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// Find pack impls whose fixed prefix matches src's leading params.
|
||||
var matched_idx: ?usize = null;
|
||||
for (pack_entries.items, 0..) |entry, i| {
|
||||
const ent_info = table.get(entry.source_pack_ty);
|
||||
// Pack impls always wear a closure (resolveClosureType routes
|
||||
// both Closure and the future Fn pack forms through
|
||||
// closureTypePack); a function-typed pack impl is not produced
|
||||
// by current parser shapes.
|
||||
if (ent_info != .closure) continue;
|
||||
const ent_ci = ent_info.closure;
|
||||
const pack_start = ent_ci.pack_start orelse continue;
|
||||
// Fixed prefix must fit within the source's params.
|
||||
if (pack_start > src_params.len) continue;
|
||||
var prefix_ok = true;
|
||||
var i_fix: u32 = 0;
|
||||
while (i_fix < pack_start) : (i_fix += 1) {
|
||||
if (ent_ci.params[i_fix] != src_params[i_fix]) {
|
||||
prefix_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefix_ok) continue;
|
||||
// Return type: if the impl's return is a generic var
|
||||
// (ret_var_name set), any source return binds; otherwise it
|
||||
// must equal the source's return exactly.
|
||||
if (entry.ret_var_name == null and ent_ci.ret != src_ret) continue;
|
||||
// First match wins for v1; concrete-wins-over-pack already
|
||||
// happened by the caller checking concrete first. Multiple
|
||||
// overlapping pack impls would be a separate diagnostic
|
||||
// (deferred — same module duplicates are caught at registration).
|
||||
matched_idx = i;
|
||||
break;
|
||||
}
|
||||
const idx = matched_idx orelse return null;
|
||||
const entry = pack_entries.items[idx];
|
||||
|
||||
// Find the `convert` method.
|
||||
var convert_fd: ?*const ast.FnDecl = null;
|
||||
for (entry.methods) |m| {
|
||||
if (std.mem.eql(u8, m.name, "convert")) {
|
||||
convert_fd = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fd = convert_fd orelse return null;
|
||||
|
||||
// Build bindings. Target → dst_ty (already in the protocol's type
|
||||
// params), pack-var → src tail TypeIds, ret-var (when generic) →
|
||||
// src ret.
|
||||
const ent_pack_start = table.get(entry.source_pack_ty).closure.pack_start.?;
|
||||
const tail = src_params[ent_pack_start..];
|
||||
const tail_owned = self.alloc.dupe(TypeId, tail) catch return null;
|
||||
|
||||
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
||||
defer bindings.deinit();
|
||||
const pd = self.protocol_ast_map.get(proto_name) orelse return null;
|
||||
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
|
||||
if (entry.ret_var_name) |rv| bindings.put(rv, src_ret) catch return null;
|
||||
|
||||
var pack_bindings = std.StringHashMap([]const TypeId).init(self.alloc);
|
||||
defer pack_bindings.deinit();
|
||||
pack_bindings.put(entry.pack_var_name, tail_owned) catch return null;
|
||||
|
||||
// Mangled name keyed on the CONCRETE source so distinct shapes
|
||||
// monomorphise separately. Same scheme as the concrete path:
|
||||
// "<src>.convert__<dst>".
|
||||
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
|
||||
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
||||
}) catch return null;
|
||||
|
||||
self.xx_reentrancy.put(guard_key, {}) catch {};
|
||||
defer _ = self.xx_reentrancy.remove(guard_key);
|
||||
|
||||
if (!self.lowered_functions.contains(mangled)) {
|
||||
const saved_pack = self.pack_bindings;
|
||||
self.pack_bindings = pack_bindings;
|
||||
defer self.pack_bindings = saved_pack;
|
||||
self.monomorphizeFunction(fd, mangled, &bindings);
|
||||
}
|
||||
|
||||
const fid = self.resolveFuncByName(mangled) orelse return null;
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const ret_ty = func.ret;
|
||||
const params = func.params;
|
||||
var single = [_]Ref{operand};
|
||||
const final_args = self.prependCtxIfNeeded(func, single[0..]);
|
||||
self.coerceCallArgs(final_args, params);
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
}
|
||||
|
||||
/// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise
|
||||
/// the impl's `convert` method and emit a direct call. Returns null when
|
||||
/// no impl matches (caller falls back to the built-in result, which is
|
||||
@@ -11101,25 +11344,18 @@ pub const Lowering = struct {
|
||||
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null;
|
||||
const key = key_buf.items;
|
||||
|
||||
const entries = self.param_impl_map.get(key) orelse {
|
||||
// M5.A — focused diagnostic for the closure→Block case. When
|
||||
// a user writes `xx cl : Block` for a closure signature that
|
||||
// has no `Into(Block) for Closure(<sig>)` impl in stdlib or
|
||||
// user code, the generic "no Into impl" path returns silently
|
||||
// and the cast becomes a no-op. Emit a hint pointing at the
|
||||
// missing impl pattern so they know what to add.
|
||||
if (self.isClosureToBlockCast(src_ty, dst_ty)) {
|
||||
if (self.diagnostics) |diags| {
|
||||
const saved = diags.current_source_file;
|
||||
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
||||
defer diags.current_source_file = saved;
|
||||
diags.addFmt(.err, operand_node.span, "no `Into(Block) for {s}` impl — add a per-signature `__block_invoke_<sig>` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code", .{self.mangleTypeName(src_ty)});
|
||||
}
|
||||
return operand;
|
||||
// Pack-only key (proto + dst) — used if the concrete lookup misses.
|
||||
// Same prefix as the concrete key, minus the `\x00<src_mangled>` tail.
|
||||
const dst_mangled_len = self.mangleTypeName(dst_ty).len;
|
||||
const pack_key = key_buf.items[0 .. proto_name.len + 1 + dst_mangled_len];
|
||||
|
||||
const entries_opt = self.param_impl_map.get(key);
|
||||
const has_concrete = entries_opt != null and entries_opt.?.items.len > 0;
|
||||
if (!has_concrete) {
|
||||
// Concrete miss — try the pack map before emitting a diagnostic.
|
||||
if (self.tryPackImplMatch(operand, operand_node, src_ty, dst_ty, proto_name, pack_key, guard_key)) |result| {
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
if (entries.items.len == 0) {
|
||||
if (self.isClosureToBlockCast(src_ty, dst_ty)) {
|
||||
if (self.diagnostics) |diags| {
|
||||
const saved = diags.current_source_file;
|
||||
@@ -11131,6 +11367,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const entries = entries_opt.?;
|
||||
|
||||
// Filter by import visibility: only impls in modules that the current
|
||||
// file transitively imports (or the current file itself) are reachable.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/agra/projects/sx/examples/155-pack-impl-match.sx:43:21: error: no `Into(Block) for cl_s32_bool__bool` impl — add a per-signature `__block_invoke_<sig>` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code
|
||||
pack impl match ok
|
||||
|
||||
Reference in New Issue
Block a user