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

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