ERR/E5.1: bare failable fn-type param resolution + non-failable->failable widening

Two more E5.1 composition pieces:
- inferExprType .call: a callee that's a local variable of bare  type
  () now resolves to its declared return type (only
  was handled before), so  /  on the call see the failable result
  instead of .
- createClosureToBareFnAdapter now widens: when a NON-failable closure literal
  flows into a failable bare slot (∅ ⊆ slot set, success type matches), the
  adapter wraps the value into the slot's  tuple via
  lowerFailableSuccessReturn — previously rejected. The failable->non-failable
  and capturing->bare crossings stay rejected.

Adapter generation fires for closure LITERALS flowing into a bare-fn slot; a
pre-bound closure VARIABLE into a bare-fn slot is a separate coercion-site path,
still unhandled (noted in CHECKPOINT-ERR). Regression:
examples/1040-errors-failable-closure-composition. Suite: 329 passed.
This commit is contained in:
agra
2026-06-01 20:56:10 +03:00
parent 06e2685350
commit b113e03fa3
5 changed files with 65 additions and 8 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
bare ok=10 err=-2
chain ok=10 err=0
widen=10

View File

@@ -7891,10 +7891,13 @@ pub const Lowering = struct {
if (self.target_type) |tt| { if (self.target_type) |tt| {
if (!tt.isBuiltin() and self.module.types.get(tt) == .function) { if (!tt.isBuiltin() and self.module.types.get(tt) == .function) {
const slot_ret = self.module.types.get(tt).function.ret; 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 (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", .{}); 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) { } else if (ret_ty == slot_ret or widen_ok) {
const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function); // 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); return self.builder.emit(.{ .func_ref = adapter }, tt);
} else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) { } 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", .{}); 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. /// (`[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 /// Only sound for capture-free closures (a null env is correct iff the body
/// reads no captures); the caller rejects capturing closures. /// 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; var params = std.ArrayList(inst_mod.Function.Param).empty;
defer params.deinit(self.alloc); defer params.deinit(self.alloc);
const void_ptr_ty = self.module.types.ptrTo(.void); 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; 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 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); const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret);
if (fn_info.ret != .void) { if (closure_ret == fn_info.ret) {
self.builder.ret(result, fn_info.ret); if (fn_info.ret != .void) {
self.builder.ret(result, fn_info.ret);
} else {
self.builder.retVoid();
}
} else { } 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(); self.builder.finalize();
@@ -14190,12 +14204,16 @@ pub const Lowering = struct {
if (fd.return_type) |rt| return self.resolveType(rt); if (fd.return_type) |rt| return self.resolveType(rt);
return .void; 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 (self.scope) |scope| {
if (scope.lookup(bare_name)) |binding| { if (scope.lookup(bare_name)) |binding| {
if (!binding.ty.isBuiltin()) { if (!binding.ty.isBuiltin()) {
const ti = self.module.types.get(binding.ty); const ti = self.module.types.get(binding.ty);
if (ti == .closure) return ti.closure.ret; if (ti == .closure) return ti.closure.ret;
if (ti == .function) return ti.function.ret;
} }
} }
} }