Skip to content

Shell arithmetic handles integers as floats, breaking integer arithmetic #771

Description

@McDutchie

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    TODOThings to be done before releasingbugSomething is not working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions