Implicit widening landed after generics did, and the rule the two of
them left between them read off the order the arguments were written
in. (eq2? i8 i64) was refused, because $t bound to i8 and i64 into i8
can lose. (eq2? i64 i8) was accepted, because $t had already bound to
i64 and the i8 widened into the want that substitution had made
concrete. Same two values, same function, one copy at i8 refused and
one copy at i64 generated.
Neither answer was unsound -- a widen cannot change a number -- so
this is not a bug report, it is a decision that was never taken.
Taking it: implicit widening does not cross a generic binding. A
concrete argument at a variable an earlier argument already bound has
to be that type, and both orders now refuse with the same sentence,
naming the binding, the argument, and the cast to write.
Refusing is the direction that can be walked back. Letting the pair
join at the wider type is a coherent rule too, and it can be added
later without invalidating a program written under this one; the
reverse is not true.
The rule costs almost nothing because Types.widens_to admits only
numeric scalars. A variable bound inside [$t] or (Fn [$t $t] bool)
leaves a parameter no widening ever applied to, so sort-by and the
whole fn-literal path are untouched by construction. Two exceptions
keep the ergonomics: an untyped literal has no type of its own to
keep, so it still takes the variable's; and a form with no type
without a want -- (zeroed) -- is asked for its natural type through a
trial, and falls back to the want it always had when the trial
refuses.