I think the negative arity is actually necessary. Take the following example:
let tuple_sum_with_more (x, y, z) =
let sum = x + y + z in
fun a b -> sum + a + b
(* Prevent inlining/simplification *)
let f = Sys.opaque_identity tuple_sum_with_more
let s = f (1, 2, 3) 4 5
If we had used 3 as the arity of the tupled function, then the last application of
f would see a 3-argument application of a function of arity 3, and call the underlying function directly, with some unfortunate results.
On the other hand, if we used the real arity of 1, then the runtime wouldn’t know how many code pointer fields there are in the closure (it’s one code pointer for functions of arity 1 (or 0), and two otherwise, but the tupled functions need two code pointers). It’s less important since 4.12, as the arity field now also encodes information about the first environment slot so scanning the closure for GC doesn’t need to look at the actual arity, but I think there is still a bit of code in the compactor which relies on this property.
I believe any negative arity would work (it could be -1 instead of -3 in this example), but I guess there’s no harm in storing the additional information.
As a side note, Flambda doesn’t use the same compilation scheme and will never generate closures with negative arity. Instead, it generates a specialised version of the
caml_tuplify function for each tupled function, which is then allocated in a regular closure of arity 1. Going back to your example,
let triple_sum (x, y, z) = x + y + z is compiled to:
let triple_sum_direct x y z = x + y + z
let triple_sum (x, y, z) = triple_sum_direct x y z [@@inline always]
The same scheme could be used with Closure, but with a performance cost (the tuple allocation wouldn’t be removed anymore, and there are a few corner cases where the function would be inlined currently but fall outside the threshold with this version).