From c917f92509b69a169a62d9d3d504af17ebc4cd7a Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 16:28:52 +0300 Subject: [PATCH] =?UTF-8?q?ffi=20M5.A.next.2b.fu2.B:=20generic=20pack-fn?= =?UTF-8?q?=20return=20=E2=80=94=20infer=20ret=5Fty=20from=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix for follow-up #2 from step 2b. When a pack-fn declares `(..\$args) -> \$R` (return type a generic name), the mono now infers ret_ty from the body's first explicit `return X;` or falls back to the tail expression of an arrow-form body. Plumbing in src/ir/lower.zig: - `inferPackBodyReturnType(body)` walks the body via the existing `findReturnValueType` helper (return stmts) and falls through to `inferExprType` on the tail expression for arrow-form / tail-expr bodies. - `monomorphizePackFn` now pre-installs `pack_arg_nodes` and `pack_param_count` BEFORE resolving the return type so the inference can substitute `args[]` to call-site arg AST nodes during type lookup. - Generic-ret detection: `fd.return_type` AST node is a `type_expr` with `is_generic = true`. Concrete returns stay on the standard `resolveReturnType` path. `examples/159-pack-generic-ret.sx` flips from `0 0` (silent- zero coercion through opaque struct ret_ty) to `42 99`. 198/198 example tests + `zig build test` green. --- src/ir/lower.zig | 66 +++++++++++++++++++------ tests/expected/159-pack-generic-ret.txt | 2 +- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 780ba4d..9ff828a 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -8066,6 +8066,27 @@ pub const Lowering = struct { return self.builder.constInt(0, .void); } + /// Infer the return type of a pack-fn body for the generic-`$R` + /// case. Walks the body looking for the first concrete return + /// type: a `return X;` statement's value type, or — failing that — + /// the tail expression of an arrow-form body. Caller must have + /// `pack_arg_nodes` installed so `args[]` substitutes during + /// inference. Falls back to `.s64` if nothing concrete is found + /// (matches the broader "default to .s64" convention elsewhere). + fn inferPackBodyReturnType(self: *Lowering, body: *const Node) TypeId { + // First try explicit `return X;` — walks past structured + // control flow but stops at nested fn / lambda bodies. + if (self.findReturnValueType(body)) |ty| return ty; + // Arrow-form / tail-expression body: the body IS the value. + // For block bodies whose last stmt is an expression, walk down. + if (body.data == .block) { + const stmts = body.data.block.stmts; + if (stmts.len == 0) return .void; + return self.inferExprType(stmts[stmts.len - 1]); + } + return self.inferExprType(body); + } + /// Per-call-shape monomorphisation entry for pack-fns /// (`isPackFn(fd) == true`). Computes a mangled name from the /// call-site arg types, builds the mono if it's not cached, and @@ -8169,13 +8190,12 @@ pub const Lowering = struct { self.builder.inst_counter = saved_counter; } - const ret_ty = self.resolveReturnType(fd); - self.target_type = ret_ty; - const wants_ctx = self.funcWantsImplicitCtx(fd); // Synthesise pack-param names + AST ident nodes used to bind - // `args[]` substitutions during body lowering. + // `args[]` substitutions during body lowering. Built + // BEFORE return-type resolution so the generic-`$R` path can + // pre-install the binding for type inference. var pack_synth_names = std.ArrayList([]const u8).empty; defer pack_synth_names.deinit(self.alloc); var pack_arg_idents = std.ArrayList(*const Node).empty; @@ -8191,6 +8211,32 @@ pub const Lowering = struct { pack_arg_idents.append(self.alloc, ident_node) catch return; } + // Resolve return type. When the declared type is a generic + // name (e.g. `(..$args) -> $R`), `resolveReturnType` would + // return an opaque struct TypeId and the mono's signature + // would be wrong. Pre-install the pack bindings + infer the + // ret type from the body's tail expression / first explicit + // `return X;` instead. + var pre_pan = std.StringHashMap([]const *const Node).init(self.alloc); + defer pre_pan.deinit(); + pre_pan.put(pack_name, pack_arg_idents.items) catch return; + var pre_ppc = std.StringHashMap(u32).init(self.alloc); + defer pre_ppc.deinit(); + pre_ppc.put(pack_name, @intCast(arg_types.len)) catch return; + self.pack_arg_nodes = pre_pan; + self.pack_param_count = pre_ppc; + + const declared_is_generic_ret = blk: { + const rt = fd.return_type orelse break :blk false; + if (rt.data != .type_expr) break :blk false; + break :blk rt.data.type_expr.is_generic; + }; + const ret_ty: TypeId = if (declared_is_generic_ret) + self.inferPackBodyReturnType(fd.body) + else + self.resolveReturnType(fd); + self.target_type = ret_ty; + // Param list: ctx (if needed) + fixed prefix + N pack params. // NOT deinit'd — `params.items` is stored by reference in // `Function.init` and read back later via `func.params`. @@ -8248,16 +8294,8 @@ pub const Lowering = struct { param_idx += 1; } - // Install pack bindings for the body lowering. - var pan_map = std.StringHashMap([]const *const Node).init(self.alloc); - defer pan_map.deinit(); - pan_map.put(pack_name, pack_arg_idents.items) catch return; - self.pack_arg_nodes = pan_map; - - var ppc_map = std.StringHashMap(u32).init(self.alloc); - defer ppc_map.deinit(); - ppc_map.put(pack_name, @intCast(arg_types.len)) catch return; - self.pack_param_count = ppc_map; + // Pack bindings remain installed from the pre-resolution + // (generic-`$R`) inference step above. No need to reinstall. if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); diff --git a/tests/expected/159-pack-generic-ret.txt b/tests/expected/159-pack-generic-ret.txt index b748e2d..efc7c1a 100644 --- a/tests/expected/159-pack-generic-ret.txt +++ b/tests/expected/159-pack-generic-ret.txt @@ -1 +1 @@ -0 0 +42 99