Files
sx/issues/0177-array-element-closure-direct-call-crashes.md
agra 555ccdc024 feat: parenthesized type grouping — (T) groups, (T,) is a 1-tuple (issue 0177)
In type position, parentheses now mirror value position: (T) (a single
unnamed element, no trailing comma) is a GROUPING that resolves to the
inner type; (T,) is a 1-tuple; (A, B) a 2-tuple; named (x: T) and spread
(..Ts) stay tuples; (...) -> R stays a function type. This lets a
closure/optional/function type be parenthesized for readability without
silently becoming a 1-tuple:
  [1](Closure(i64,i64) -> i64)   // array of closures (issue 0177) -> 7
  ?(?i64)                        // genuine nested optional (issue 0165 intent)

Parser: src/parser.zig returns the inner node for a single unnamed
non-spread no-trailing-comma parenthesized type. formatTypeName (both
generic.zig diagnostics + types.zig reflection) now render a 1-tuple as
(T,) so the spelling is unambiguous and diagnostics are self-consistent.
The 0165 coerce/stmt note reworded accordingly.

specs.md §Type Syntax updated; basic/0036 wrap return -> (i64,); obsolete
diagnostic 1195 removed (?(?i64) now compiles); regression
examples/types/0201-types-parenthesized-type-grouping.sx added; 0414 .ir
golden regenerated for the (T,) rendering. Resolves 0177; updates
0165/0170. Verified by 3 adversarial reviews; suite 792/0.
2026-06-23 10:43:47 +03:00

2.9 KiB

0177 — calling a closure stored in an array element directly (fns[i](args)) crashes / miscompiles

RESOLVED via parenthesized-type grouping. The repro [1](Closure(i64,i64) -> i64) = .[ add ] was not an array of closures — under the old rule (Closure(...) -> R) was a 1-tuple, so it was an array of 1-tuples and fns[0](...) tried to call a tuple → LLVM "Called function must be a pointer!". Per the user's direction, parentheses in TYPE position are now a GROUPING (mirroring value position): (T) (no trailing comma) resolves to the inner type, (T,) is the 1-tuple. So [1](Closure(...) -> R) is now an array of closures and fns[0](3,4) returns 7. (The canonical non-paren [1]Closure(...) -> R = .[ add ] already worked.) Implemented in src/parser.zig (single unnamed non-spread element, no trailing comma → return the inner type node). Regression: examples/types/0201-types-parenthesized-type-grouping.sx. specs.md §Type Syntax updated. Verified by 3 adversarial reviews; suite 792/0.

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

#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).closurecall_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.