fix: dispatch unwrapped optional-closure call g!() through call_closure (issue 0170)
Calling through an unwrapped optional closure (g!()) crashed with LLVM
'Called function must be a pointer!': the indirect-call catch-all else
arm emitted call_indirect on the whole {fn,env} closure struct with a
hardcoded .i64 return. The else arm now inspects inferExprType(callee):
a .closure callee dispatches through call_closure (threads env + ctx via
the [ctx, env, user_args] ABI, returns closure.ret); a plain fn pointer
uses call_indirect with the callee's real function.ret instead of i64.
The filed repro's ?(() -> void) spelling is a tuple-optional (now
diagnosed by the 0165 fix); the real ?Closure(...) layout was already
correct. Verified load-bearing (HEAD crashes) by 3 adversarial reviews,
suite 785/0. Regression: examples/closures/0311-closures-optional-closure.sx.
Filed adjacent bug 0177 (array-element closure direct call crashes).
This commit is contained in:
66
examples/closures/0311-closures-optional-closure.sx
Normal file
66
examples/closures/0311-closures-optional-closure.sx
Normal file
@@ -0,0 +1,66 @@
|
||||
// Optional closures: `?Closure(...) -> R`.
|
||||
//
|
||||
// Runtime repr is the sentinel form — the optional IS the closure struct
|
||||
// `{fn_ptr, env}`; has_value = `fn_ptr != null`, no separate flag word.
|
||||
// Covers: present vs null truthiness, `== null` / `!= null`, force-unwrap +
|
||||
// call-through (with and without captures), `??` coalesce, pass-as-param,
|
||||
// return, and args + non-void return.
|
||||
//
|
||||
// Regression (issue 0170): `g!()` where `g : ?Closure(...)` previously lowered
|
||||
// to a `call_indirect` that treated the closure `{fn,env}` struct as a bare
|
||||
// fn pointer (LLVM "Called function must be a pointer!"); the indirect-call
|
||||
// catch-all also hardcoded an `.i64` return type. The else-arm in
|
||||
// `src/ir/lower/call.zig` now dispatches to `call_closure` when the callee
|
||||
// expression's static type is a closure, and uses the real return type.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
take :: (g: ?Closure() -> void) {
|
||||
if g { g!(); } else { print("param-absent\n"); }
|
||||
}
|
||||
|
||||
give :: () -> ?Closure() -> void {
|
||||
return () => print("from-give\n");
|
||||
}
|
||||
|
||||
main :: () {
|
||||
// With captures.
|
||||
n := 7;
|
||||
a : ?Closure() -> void = () => print("a n={}\n", n);
|
||||
if a { print("a-present\n"); } else { print("a-absent\n"); }
|
||||
a!();
|
||||
|
||||
// Without captures.
|
||||
b : ?Closure() -> void = () { print("b-called\n"); };
|
||||
if b { b!(); }
|
||||
|
||||
// Null tests absent; == / != null.
|
||||
c : ?Closure() -> void = null;
|
||||
if c { print("c-present\n"); } else { print("c-absent\n"); }
|
||||
print("c==null: {}\n", c == null);
|
||||
print("c!=null: {}\n", c != null);
|
||||
|
||||
// Reassign: null -> value -> null.
|
||||
c = () { print("c-reassigned\n"); };
|
||||
if c { c!(); }
|
||||
c = null;
|
||||
if c { print("c2-present\n"); } else { print("c2-absent\n"); }
|
||||
|
||||
// Coalesce: null falls back, present uses self.
|
||||
fallback := () { print("fallback\n"); };
|
||||
(c ?? fallback)();
|
||||
e : ?Closure() -> void = () { print("e-real\n"); };
|
||||
(e ?? fallback)();
|
||||
|
||||
// Pass as param (present + null).
|
||||
take(a);
|
||||
take(c);
|
||||
|
||||
// Return an optional closure.
|
||||
r := give();
|
||||
if r { r!(); }
|
||||
|
||||
// Args + non-void return.
|
||||
add : ?Closure(i64, i64) -> i64 = (x: i64, y: i64) => x + y;
|
||||
if add { print("add: {}\n", add!(3, 4)); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
a-present
|
||||
a n=7
|
||||
b-called
|
||||
c-absent
|
||||
c==null: true
|
||||
c!=null: false
|
||||
c-reassigned
|
||||
c2-absent
|
||||
fallback
|
||||
e-real
|
||||
a n=7
|
||||
param-absent
|
||||
from-give
|
||||
add: 7
|
||||
@@ -1,5 +1,22 @@
|
||||
# 0170 — closure optional `?(() -> ...)` alloca is sized one word, truncating the `{fn,env}` closure value
|
||||
|
||||
> **RESOLVED** (root cause differs from the title's hypothesis). Two findings:
|
||||
> (1) the filed repro's `?(() -> void)` spelling is a TUPLE-optional (the `(T)`
|
||||
> 1-tuple rule), now correctly diagnosed by the issue-0165 fix — not a closure
|
||||
> bug. (2) The REAL closure-optional `?Closure(args) -> R` layout was already
|
||||
> correct (sentinel form: the value IS the closure `{fn,env}`, has_value =
|
||||
> `fn_ptr != null`); `if g` / `== null` / `!= null` already worked. The genuine
|
||||
> bug was calling through an unwrapped optional closure `g!()` — the indirect-call
|
||||
> catch-all `else` arm (`src/ir/lower/call.zig`) emitted `call_indirect` on the
|
||||
> whole `{fn,env}` struct (LLVM "Called function must be a pointer!") with a
|
||||
> hardcoded `.i64` return. Fix: the `else` arm inspects `inferExprType(callee)` —
|
||||
> `.closure` → `call_closure` (threads env+ctx, returns `closure.ret`); else →
|
||||
> `call_indirect` with the callee's real `function.ret`. Verified load-bearing
|
||||
> (HEAD crashes) by 3 adversarial reviews; suite 785/0. Regression:
|
||||
> `examples/closures/0311-closures-optional-closure.sx`. (Adjacent pre-existing
|
||||
> bug found + filed: 0177 — array-element closure direct call `fns[i](args)`
|
||||
> crashes.)
|
||||
|
||||
## Symptom
|
||||
|
||||
An optional of a closure (`?(() -> void)`, `?Fn`) is mis-laid-out: the optional
|
||||
|
||||
41
issues/0177-array-element-closure-direct-call-crashes.md
Normal file
41
issues/0177-array-element-closure-direct-call-crashes.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 0177 — calling a closure stored in an array element directly (`fns[i](args)`) crashes / miscompiles
|
||||
|
||||
## Symptom
|
||||
|
||||
A closure (or `Closure(...)`-typed value) stored in an array, called DIRECTLY via
|
||||
index (`fns[i](args)`), does not dispatch through the closure ABI: it emits a bare
|
||||
`call_indirect` on the whole `{fn,env}` struct → LLVM "Called function must be a
|
||||
pointer!" (verify fail) for some return/arg shapes, or returns garbage for others.
|
||||
Pre-existing (reproduces on master); distinct from issue 0170 (which fixed the
|
||||
unwrap-through-optional call `g!()`). Here the callee is a non-optional closure
|
||||
reached via array index, called directly without unwrap.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
add :: (a: i64, b: i64) -> i64 { return a + b; }
|
||||
main :: () {
|
||||
fns : [1](Closure(i64, i64) -> i64) = .{ add };
|
||||
print("{}\n", fns[0](3, 4)); // LLVM "Called function must be a pointer!" — expected 7
|
||||
}
|
||||
```
|
||||
|
||||
Expected: `7`. Observed: LLVM verification failure (or, for other shapes, garbage
|
||||
return / f64-arg verify failure).
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
`src/ir/lower/call.zig`: issue 0170 added closure-vs-fn-pointer dispatch to the
|
||||
indirect-call catch-all `else` arm via `inferExprType(callee)` → `.closure` →
|
||||
`call_closure`. A direct call whose callee is an ARRAY-INDEX expression
|
||||
(`fns[0]`) of closure type apparently does not reach that dispatch — either it
|
||||
takes an earlier call arm that still emits `call_indirect`, or
|
||||
`inferExprType(index_expr)` does not return `.closure` so the `else` arm falls to
|
||||
the fn-pointer path. Trace which arm `fns[0](args)` lowers through and ensure a
|
||||
closure-typed callee — regardless of whether it's a bare ident, field access,
|
||||
index, or call result — dispatches through `call_closure` (threading env + ctx
|
||||
via the `[ctx, env, user_args]` ABI). Compare with the working `arr[i]!()`
|
||||
(unwrap) path. Follow the no-silent-fallback rule. Verify: `fns[0](3,4)` → 7;
|
||||
array-of-closure with captures; non-i64 returns (void/f64/struct); f64 args.
|
||||
Add an `examples/closures/03xx-array-of-closures-call.sx` regression.
|
||||
@@ -1356,10 +1356,38 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
return self.builder.enumInit(tag, payload, target);
|
||||
},
|
||||
else => {
|
||||
// Indirect call through expression
|
||||
// Indirect call through expression. The callee can be a plain
|
||||
// function pointer OR a closure value (e.g. `g!()` where
|
||||
// `g : ?Closure(...)` — the force-unwrap yields the closure
|
||||
// struct). Inspect the callee's static type so we emit the
|
||||
// right op: `call_closure` splits `{fn_ptr, env}` and threads
|
||||
// env (and implicit ctx), whereas `call_indirect` would treat
|
||||
// the whole struct as a bare fn pointer and miscompile.
|
||||
const callee_ty = self.inferExprType(c.callee);
|
||||
if (!callee_ty.isBuiltin()) {
|
||||
const cti = self.module.types.get(callee_ty);
|
||||
if (cti == .closure) {
|
||||
const callee_ref = self.lowerExpr(c.callee);
|
||||
// Prepend implicit ctx for the sx-side closure call ABI
|
||||
// (emit_llvm builds the call as [ctx, env, user_args]).
|
||||
const owned = if (self.implicit_ctx_enabled) blk: {
|
||||
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
|
||||
arr[0] = self.current_ctx_ref;
|
||||
@memcpy(arr[1..], args.items);
|
||||
break :blk arr;
|
||||
} else self.alloc.dupe(Ref, args.items) catch unreachable;
|
||||
return self.builder.emit(.{ .call_closure = .{ .callee = callee_ref, .args = owned } }, cti.closure.ret);
|
||||
}
|
||||
}
|
||||
// Plain function-pointer indirect call. Use the callee's static
|
||||
// return type when known instead of a hardcoded `.i64` default.
|
||||
const ret_ty: TypeId = if (!callee_ty.isBuiltin()) blk: {
|
||||
const cti = self.module.types.get(callee_ty);
|
||||
break :blk if (cti == .function) cti.function.ret else .i64;
|
||||
} else .i64;
|
||||
const callee_ref = self.lowerExpr(c.callee);
|
||||
const owned = self.alloc.dupe(Ref, args.items) catch unreachable;
|
||||
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, .i64);
|
||||
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, ret_ty);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user