The C standard says (1, 2):
When a value of integer type is converted to a real floating type, if the value being converted can be represented exactly in the new type, it is unchanged. If the value being converted is in the range of values that can be represented but cannot be represented exactly, the result is either the nearest higher or nearest lower representable value, chosen in an implementation-defined manner. If the value being converted is outside the range of values that can be represented, the behavior is undefined.
Thing is, ksh internally typecasts all values for shell arithmetic to Sfdouble_t, a.k.a. _ast_fltmax_t as derived by the features/float test, i.e., the system's maximum-size float type.
So, all arithmetic is internally done with long double or double values (depending on the system). See streval.c and arith.c. This breaks long integers, as their values may be too large to be represented exactly by the Sfdouble_t type, in which case we hit the implementation-defined behaviour case and the number will be somehow approximated. This is disastrous, because integer calculations that remain within type range must always be exact.
The problem is particularly terrible on ARM systems, whose hardware does not support long double, so Sfdouble_t is double. E.g., on my Mac with an M1 processor:
$ getconf LLONG_MAX
9223372036854775807
$ typeset -lui i=9223372036854775807 # long unsigned int; this value should be only half its max
$ echo $i
9223372036854775807
$ echo $((i - 1))
9223372036854775807
$ echo $((i - 2))
9223372036854775807
$ echo $((i - 100))
9223372036854775807
$ echo $((i - 1000))
9223372036854774784
$ echo $((i - 100000))
9223372036854675456
$ echo $((i - 1000000))
9223372036853775360
$ echo $((i - 10000000))
9223372036844775424
$ echo $((i - 100000000))
9223372036754776064
Whereas, on x86_64, we get inexact representations when we go beyond LLONG_MAX:
$ typeset -lui i=9223372036854775807
$ echo $((i+1))
9.22337203685477581e+18
$ echo $((i-1))
9223372036854775806
What this shows is that the whole arithmetic subsystem is broken by design. Internally converting integers to floats and back is bogus, because even the largest float type cannot store all the possible integer values. Integers must be stored and calculated as integers, and nothing else.
We need to find a way, somehow, of making that happen, while still supporting floating point arithmetic as well. But the streval.c and arith.c code is so inscrutable, I'm not very much closer to understanding it now than I was when I forked ksh four years ago.
The C standard says (1, 2):
Thing is, ksh internally typecasts all values for shell arithmetic to
Sfdouble_t, a.k.a._ast_fltmax_tas derived by thefeatures/floattest, i.e., the system's maximum-size float type.So, all arithmetic is internally done with
long doubleordoublevalues (depending on the system). See streval.c and arith.c. This breaks long integers, as their values may be too large to be represented exactly by theSfdouble_ttype, in which case we hit the implementation-defined behaviour case and the number will be somehow approximated. This is disastrous, because integer calculations that remain within type range must always be exact.The problem is particularly terrible on ARM systems, whose hardware does not support
long double, soSfdouble_tisdouble. E.g., on my Mac with an M1 processor:Whereas, on x86_64, we get inexact representations when we go beyond
LLONG_MAX:What this shows is that the whole arithmetic subsystem is broken by design. Internally converting integers to floats and back is bogus, because even the largest float type cannot store all the possible integer values. Integers must be stored and calculated as integers, and nothing else.
We need to find a way, somehow, of making that happen, while still supporting floating point arithmetic as well. But the streval.c and arith.c code is so inscrutable, I'm not very much closer to understanding it now than I was when I forked ksh four years ago.