File four issue write-ups discovered alongside the 0179 work: - 0185: binary-op operand auto-unwrap silently miscompiles a NULL ?T - 0186: closure VALUE call does not coerce arg to ?T parameter - 0187: lambda with inferred return type + block body with early returns mis-infers its return type - 0188: closure-VALUE calls skip argument validation (arity + tuple spread)
4.6 KiB
0186 — calling a closure VALUE with a ?T parameter does not coerce the argument
RESOLVED. Root cause as diagnosed below: the closure-VALUE call path in
src/ir/lower/call.ziglowered args without coercing to the closure's declared parameter types. Two-part fix: (1)resolveCallParamTypesnow returns the closure/function value's param types for an identifier callee bound to a closure/fn VALUE in scope (so args lower with the righttarget_type; precedes function-name resolution since a local value shadows a function); (2) new free fncoerceClosureCallArgscoerces each already-lowered user arg to the closure's param type viacoerceToType, applied at all threecall_closureemission sites (local-variable callee, struct-field callee, force-unwrap/expr callee) AND the local function-pointercall_indirectpath (which had the identical gap — an adversarial review flagged that the.functionbranch of (1) typed fn-ptr args but never coerced them). Now a concrete arg wraps present,null→ absent — matching a top-level fn call, for both closure values and fn-pointer values. Regression:examples/closures/0312-closure-optional-param-arg-coercion.sx(local + struct-field closure + fn-pointer value, concrete + null args).Discovered while verifying (separate, NOT fixed here): a lambda with an INFERRED return type (no
-> T) and a block body with earlyreturns mis-infers its return type (LLVM verifier failure) even with no optionals. Filed as issue 0187. (The 0312 regression uses an explicit-> i64to avoid it.)
Symptom
When a closure value/variable (a :=-bound lambda, or any closure passed as a
value) has a parameter of optional type ?T, the call site does NOT coerce the
argument to ?T:
- A concrete argument (
pick(7)) is NOT wrapped to a present?i64— inside the body the param reads as ABSENT (p == nullis true), so the closure silently returns the wrong branch. - A
nullargument (pick(null)) lowersnullas a bareptr nullagainst a{i64, i1}parameter, which fails LLVM verification:Call parameter type does not match function signature! ptr null { i64, i1 }.
The SAME signature/body as a TOP-LEVEL function works correctly (e.g. issue
0900's guard), so the bug is specific to the closure-VALUE call path
(src/ir/lower/call.zig's closure/fn-pointer call lowering), not optionals or
flow narrowing. Found during the adversarial review of issues 0179 / 0185.
Reproduction
#import "modules/std.sx";
norm :: (p: ?i64) -> i64 { if p == null { return -1; } return 99; }
main :: () {
pick := (p: ?i64) -> i64 => {
if p == null { return -1; }
return 99;
};
print("pick 7: {}\n", pick(7)); // prints -1 (WRONG — should be 99; arg arrives absent)
print("norm 7: {}\n", norm(7)); // prints 99 (top-level fn, correct)
// print("pick null: {}\n", pick(null)); // LLVM verifier failure: ptr null vs {i64,i1}
}
Expected: pick(7) prints 99 (the 7 wraps to a present ?i64), and
pick(null) compiles (the null lowers to an absent ?i64), matching the
top-level norm.
Root cause (hypothesis)
The closure-value call path in src/ir/lower/call.zig lowers each argument and
passes it to the closure's trampoline WITHOUT running the coerceToType step
that the normal sx-to-sx call path applies against the callee's declared
parameter types. So a T → ?T wrap (and null → ?T) never happens for a
closure value's optional param. A top-level fn call coerces args to param types,
which is why norm works.
Investigation prompt
In src/ir/lower/call.zig, find the closure-value / fn-pointer call lowering
(where %cl.fn/%cl.env trampolines are invoked — grep for cl.fn / the
closure-call branch). Confirm it lowers args without coercing to the closure
type's parameter types. The closure's parameter types are available from the
Closure(...)/closure TypeInfo. Coerce each lowered argument to the
corresponding parameter type via self.coerceToType(arg, arg_ty, param_ty)
before building the call — mirroring the sx-to-sx call path. Verify:
- The repro above:
pick(7)→99,pick(null)compiles and the body sees absent. - No regression in existing closure examples (
examples/closures/). - Add a regression
examples/closures/03xx-closure-optional-param.sxcovering a concrete arg (wraps present) and anullarg (absent) into a closure-value?Tparam.
Note: this is purely an argument-coercion gap at the closure-value call site; it is unrelated to the implicit-optional-unwrap family (issues 0179 / 0185), which is already fixed.