diff --git a/examples/1040-errors-failable-closure-composition.sx b/examples/1040-errors-failable-closure-composition.sx new file mode 100644 index 0000000..b6ef790 --- /dev/null +++ b/examples/1040-errors-failable-closure-composition.sx @@ -0,0 +1,34 @@ +// Failable closure composition (ERR E5.1): a closure LITERAL passed as a +// function-type argument and called inside the callee. Covers a bare failable +// fn-type param (`cb: (T) -> (U, !)`), the idiomatic `Closure(...)` param +// (try-propagated), and ∅-widening of a NON-failable closure literal into a +// failable slot (the generated adapter wraps the value into `{value, 0}`). +// +// NOTE: the adapter is generated when the closure LITERAL flows directly into +// the bare-fn slot. Passing a pre-bound closure *variable* into a bare-fn slot +// is a separate coercion-site path, not yet handled — see CHECKPOINT-ERR. + +#import "modules/std.sx"; + +E :: error { Neg } + +bare :: (cb: (s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch e -1; } +chain :: (cb: Closure(s64) -> (s64, !E), n: s64) -> (s64, !E) { return try cb(n); } + +dbl :: (x: s64) -> (s64, !E) { if x < 0 { raise error.Neg; } return x * 2; } + +main :: () -> s32 { + // failable closure literal through a bare fn-type param (matching ABI) + print("bare ok={} err={}\n", + bare(closure((x: s64) -> (s64, !E) { if x < 0 { raise error.Neg; } return x * 2; }), 5), + bare(closure((x: s64) -> (s64, !E) => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2 + + // Closure(...) param, try-propagated, then caught at the call site + print("chain ok={} err={}\n", + chain(closure((x: s64) -> (s64, !E) => x + 6), 4) catch e 0, // 10 + chain(closure((x: s64) -> (s64, !E) { raise error.Neg; }), 1) catch e 0); // 0 + + // NON-failable closure literal widened into the failable bare slot + print("widen={}\n", bare(closure((x: s64) -> s64 => x + 1), 9)); // 10 + return 0; +} diff --git a/examples/expected/1040-errors-failable-closure-composition.exit b/examples/expected/1040-errors-failable-closure-composition.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/1040-errors-failable-closure-composition.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1040-errors-failable-closure-composition.stderr b/examples/expected/1040-errors-failable-closure-composition.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1040-errors-failable-closure-composition.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1040-errors-failable-closure-composition.stdout b/examples/expected/1040-errors-failable-closure-composition.stdout new file mode 100644 index 0000000..46c0eee --- /dev/null +++ b/examples/expected/1040-errors-failable-closure-composition.stdout @@ -0,0 +1,3 @@ +bare ok=10 err=-2 +chain ok=10 err=0 +widen=10 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 6c1a507..2adf685 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -7891,10 +7891,13 @@ pub const Lowering = struct { if (self.target_type) |tt| { if (!tt.isBuiltin() and self.module.types.get(tt) == .function) { const slot_ret = self.module.types.get(tt).function.ret; + const widen_ok = self.errorChannelOf(slot_ret) != null and self.errorChannelOf(ret_ty) == null and self.failableSuccessType(slot_ret) == ret_ty; if (capture_list.len > 0) { if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "a capturing closure cannot be passed as a bare function pointer; declare the parameter type as `Closure(...)` so its environment is carried", .{}); - } else if (ret_ty == slot_ret) { - const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function); + } else if (ret_ty == slot_ret or widen_ok) { + // Matching ABI, or a non-failable closure widening into a + // failable slot (∅ ⊆ slot set) — the adapter wraps {value, 0}. + const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function, ret_ty, lam.body.span); return self.builder.emit(.{ .func_ref = adapter }, tt); } else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) { if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "failable closure cannot be assigned to a non-failable function-type slot; foreign code can't observe the error channel — handle the error in a wrapper closure that absorbs it", .{}); @@ -8030,7 +8033,12 @@ pub const Lowering = struct { /// (`[ctx?] + user-params`) and forwards to the closure fn with a null env. /// Only sound for capture-free closures (a null env is correct iff the body /// reads no captures); the caller rejects capturing closures. - fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo) FuncId { + /// + /// When `closure_ret` differs from `fn_info.ret`, this is the ∅-widening + /// case (a non-failable closure into a failable slot): the closure returns + /// the success value and the adapter wraps it into the slot's `{value, 0}` + /// failable tuple (ERR E5.1 non-failable→failable widening). + fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo, closure_ret: TypeId, span: ast.Span) FuncId { var params = std.ArrayList(inst_mod.Function.Param).empty; defer params.deinit(self.alloc); const void_ptr_ty = self.module.types.ptrTo(.void); @@ -8074,11 +8082,17 @@ pub const Lowering = struct { call_args.append(self.alloc, Ref.fromIndex(@intCast(ctx_slots + i))) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; - const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, fn_info.ret); - if (fn_info.ret != .void) { - self.builder.ret(result, fn_info.ret); + const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret); + if (closure_ret == fn_info.ret) { + if (fn_info.ret != .void) { + self.builder.ret(result, fn_info.ret); + } else { + self.builder.retVoid(); + } } else { - self.builder.retVoid(); + // ∅-widening: closure returns the success value; wrap `{value, 0}` + // into the slot's failable tuple. + self.lowerFailableSuccessReturn(result, fn_info.ret, span); } self.builder.finalize(); @@ -14190,12 +14204,16 @@ pub const Lowering = struct { if (fd.return_type) |rt| return self.resolveType(rt); return .void; } - // Check if callee is a local closure variable — extract return type + // Check if callee is a local closure / function-type variable + // (e.g. a `cb: Closure(...) -> R` or bare `cb: (T) -> R` + // parameter) — extract its declared return type so `try` / + // `catch` on the call see the (possibly failable) result. if (self.scope) |scope| { if (scope.lookup(bare_name)) |binding| { if (!binding.ty.isBuiltin()) { const ti = self.module.types.get(binding.ty); if (ti == .closure) return ti.closure.ret; + if (ti == .function) return ti.function.ret; } } }