ffi M5.A.next.5.2.B: generic Into(Block) impl — make-green

Adds the generic `impl Into(Block) for Closure(..$args) -> $R`
in `library/modules/std/objc_block.sx` alongside the existing
hand-rolled `Closure() -> void` and `Closure(bool) -> void`
impls. The convert body is a single
`#insert build_block_convert($args, $R);` — per-call-shape
monomorphisation re-runs the builder so each closure shape gets
its dedicated nested `callconv(.c)` trampoline + Block literal.

The impl-mono path threads pack types through
`pack_bindings[args]` and the single-type return through
`type_bindings[R]`. Both need to be visible to the body's
`$args` / `$R` expression-position references — the existing
lowering only consulted `pack_arg_types` (set by pack-fn mono,
not by tryPackImplMatch). Two small extensions:

- `lowerExpr`'s `.comptime_pack_ref` arm now consults
  `pack_arg_types` → `pack_bindings` → `type_bindings` in order,
  treating a `type_bindings` hit as a single `const_type(T)`
  value rather than the slice form.
- `resolveTypeArg` grows a `.comptime_pack_ref` arm that maps
  the same name through `type_bindings` so type-arg positions
  (e.g. inside `type_name(...)` in the builder body) resolve
  the bound single Type.
- `type_bridge.isTypeShapedAstNode` lists `comptime_pack_ref`
  and `pack_index_type_expr` as type-shaped so
  `buildTypeBindings`'s strategy-1 explicit-arg path picks
  them up when calling a `$T: Type`-generic fn.

`examples/177-generic-into-block.sx` flips green: a
`Closure(s64, s64) -> void` (no hand-rolled impl) is converted
through the generic impl, its block invoked via a typed
`callconv(.c)` fn-pointer, and the closure's side effects land
in the host globals. Hand-rolled impls remain for `()` and
`(bool)` shapes; 5.3 deletes those once a focused test covers
their behaviour through the generic path. Suite at 217/217.
This commit is contained in:
agra
2026-05-27 21:58:33 +03:00
parent f5342e9fcc
commit 165b621ab3
3 changed files with 57 additions and 11 deletions

View File

@@ -108,6 +108,20 @@ impl Into(Block) for Closure(bool) -> void {
}
}
// Generic impl: covers any closure shape not handled by the
// hand-rolled per-signature impls above. The compiler
// monomorphises this body per call shape; inside each mono,
// `$args` is the bound pack of arg types and `$R` is the bound
// return type. The `#insert` evaluates `build_block_convert` at
// comptime and substitutes the resulting source — a nested
// `callconv(.c)` trampoline + the Block literal that points its
// `invoke` slot at it.
impl Into(Block) for Closure(..$args) -> $R {
convert :: (self: Closure(..$args) -> $R) -> Block {
#insert build_block_convert($args, $R);
}
}
// Comptime builder for the generic `Into(Block) for Closure(..$args)
// -> $R` impl body. Receives the closure's per-position pack types
// (`args`, a comptime `[]Type`) and the closure's return type

View File

@@ -2058,19 +2058,37 @@ pub const Lowering = struct {
// directly. Step 4 final slice — lets builder fns walk the
// whole pack at interp time.
.comptime_pack_ref => |cpr| blk: {
const pat = self.pack_arg_types orelse {
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name});
// `$<name>` is overloaded in expression position:
// - Inside a pack-fn mono (or a `tryPackImplMatch`
// impl mono), `name` is a pack binding → slice of
// element types (`[]Type` lowered as `[]Any`).
// - Inside an impl mono whose impl pattern bound a
// single-type generic (`$R: Type` in
// `Closure(..$args) -> $R`), `name` is in
// `type_bindings` → single `const_type(R)` value.
// Pack arg types are checked first (the slice form),
// then pack_bindings (the impl-mono mirror), then
// type_bindings (single-type binding); only if all
// miss is it a real "outside an active binding" error.
if (self.pack_arg_types) |pat| {
if (pat.get(cpr.pack_name)) |arg_tys| {
break :blk self.buildPackSliceValue(arg_tys);
}
break :blk self.builder.constNull(self.module.types.sliceOf(.any));
};
const arg_tys = pat.get(cpr.pack_name) orelse {
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "pack reference ${s} has no active binding", .{cpr.pack_name});
}
if (self.pack_bindings) |pb| {
if (pb.get(cpr.pack_name)) |arg_tys| {
break :blk self.buildPackSliceValue(arg_tys);
}
break :blk self.builder.constNull(self.module.types.sliceOf(.any));
};
break :blk self.buildPackSliceValue(arg_tys);
}
if (self.type_bindings) |tb| {
if (tb.get(cpr.pack_name)) |ty| {
break :blk self.builder.constType(ty);
}
}
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name});
}
break :blk self.builder.constNull(self.module.types.sliceOf(.any));
},
// Pack-index in expression position: `$<pack>[<lit>]` →
// `const_type(arg_types[index])`. Yields a comptime-only
@@ -9099,6 +9117,18 @@ pub const Lowering = struct {
}
return .void;
}
// Bare `$<name>` in a type-arg position. Single-type generic
// bindings (`$R: Type` in `Closure(..$args) -> $R`) live in
// `type_bindings`; if the name is bound there, return the
// bound TypeId directly. Pack bindings would otherwise resolve
// to a slice value, not a single Type — the caller (e.g.
// `type_name(...)`) expects a single arg.
if (node.data == .comptime_pack_ref) {
const cpr = node.data.comptime_pack_ref;
if (self.type_bindings) |tb| {
if (tb.get(cpr.pack_name)) |ty| return ty;
}
}
switch (node.data) {
.identifier => |id| {
// Check type bindings first (from generic monomorphization)

View File

@@ -357,6 +357,8 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool {
.closure_type_expr,
.tuple_type_expr,
.parameterized_type_expr,
.pack_index_type_expr,
.comptime_pack_ref,
=> true,
.identifier => |id| table.findByName(table.internString(id.name)) != null,
.tuple_literal => |tl| blk: {