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:
agra
2026-06-23 01:02:13 +03:00
parent 28bb101a4a
commit 3605165398
7 changed files with 170 additions and 2 deletions

View File

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