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:
34
examples/1040-errors-failable-closure-composition.sx
Normal file
34
examples/1040-errors-failable-closure-composition.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
bare ok=10 err=-2
|
||||||
|
chain ok=10 err=0
|
||||||
|
widen=10
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user