Numeric types

In Fable, we use F# numeric types, which are all translated to Python integers at the exception of float, double, and decimal.

Fable numbers are very nearly compatible with .NET semantics, but translating into Python types has consequences:

  • (non-standard) All floating point numbers are implemented as 64 bit (double). This makes float32 numbers more accurate than expected.

  • (non-standard) Arithmetic integers of 32 bits or less are implemented with different truncation from that expected, as whole numbers embedded within double.

  • (OK) Conversions between types are correctly truncated.

  • (OK) Bitwise operations for 64 bit and 32 bit integers are correct and truncated to the appropriate number of bits.

  • (non-standard) Bitwise operations for 16 bit and 8 bit integers use the underlying JavaScript 32 bit bitwise semantics. Results are not truncated as expected, and shift operands are not masked to fit the data type.

  • (OK) Longs have a custom implementation which is identical in semantics to .NET and truncates in 64 bits, although it is slower.

32 bit integers thus differ from .NET in two ways:

  • Underlying 52 bit precision, without expected truncation to 32 bits on overflow. Truncation can be forced if needed by >>> 0.

  • On exceeding 52 bits absolute value floating point loses precision. So overflow will result in unexpected lower order 0 bits.

The loss of precision can be seen in a single multiplication:

((1 <<< 28) + 1) * ((1 <<< 28) + 1) >>> 0

The multiply product will have internal double representation rounded to 0x0100_0000_2000_0000. When it is truncated to 32 bits by >>> 0 the result will be 0x2000_0000 not the .NET exact lower order bits value of 0x2000_0001.

The same problem can be seen where repeated arithmetic operations make the internal (non-truncated) value large. For example a linear congruence random number generator:

let rng (s:int32) = 10001*s + 12345

The numbers generated by repeated application of this to its result will all be even after the 4th pseudo-random number, when s value exceeds 2^53:

let rec randLst n s =
    match n with
    | 0 -> [s]
    | n -> s :: randLst (n-1) (rng s)

List.iter (printfn "%x") (randLst 7 1)

The resulting printed list of pseudo-random numbers does not work in Fable:

Fable

.NET

1

1

574a

574a

d524223

d524223

6a89e98c

6a89e98c

15bd0684

15bd0685

3d8b8000

3d8be20e

50000000

65ba5527

0

2458c8d0

Workarounds

  • When accurate low-order bit arithmetic is needed and overflow can result in numbers larger than 2^53 use int64, uint64, which use exact 64 bits, instead of int32, uint32.

  • Alternately, truncate all arithmetic with >>> 0 or >>> 0u as appropriate before numbers can get larger than 2^53: let rng (s:int32) = 10001*s + 12345 >>> 0

Printing

One small change from .NET in printf, sprintf, ToString. Negative signed integers are printed in hexadecimal format as sign + magnitude, in .NET they are printed as two’s complement bit patterns.