Fable: (dev2.0) Document fully numeric conversions (and make them more consistent)

Created on 22 Aug 2018  路  8Comments  路  Source: fable-compiler/Fable

This issue is a placeholder to discuss progress on improving and documenting integer numeric conversions.

I plan to add documentation but not yet since we are still maybe going to rationalise / understand current issues in numeric types. So this post documents what is understood.

Background

(1) JS uses IEEE-754 64 bit floats for all numbers. These can store signed integers up to 53 bits magnitude (in FP format). These FP integers are stored in sign/magnitude form.

(2) JS Bitwise operations do 32 bit two's complement arithmetic. They convert automatically to/from the equivalent signed FP integers. These ops truncate results to 32 bits, and mostly return FP integers in range [-2^32 .. 2^31 -1]. However >>> (logical shift right) is special and returns its result as the unsigned equivalent of the two-'s complement bits ([0..2^32 -1]).

(3) Fable stores int64, uint64 as records containing two numbers (32 bits each) and a signed / unsigned boolean flag. The MS 32 bits are signed and represent top 32 two's c bits. The bottom 32 bits are unsigned and represent the lower 32 bits of a 64 bit two's C number.

(4) FABLE stores int32, uint32 as positive/negative or all positive, respectively, FABLE FP integers.

OK, so here are the expected (from fsi) .NET numeric conversions, and what Fable2.0 does.

(-1 |> any unsigned numeric type) should be 2^N -1 where N is type width.
(2^N-1 in any unsigned type width N -> any signed type) should be -1.

examples from FSI:

(-1 |> char |> uint) = 16383u
(-1 |> char |>int) = 16383 (char is unsigned 16 bit, so that conversion determines value)
(-1 |> uint64) = 18446744073709551615uL
(-1L |> uint64) = 18446744073709551615uL
(18446744073709551615uL |> int64) = -1L
(18446744073709551615uL |> int32) = -1
(-1 |> uint32) = 4294967295u
(4294967295u |> int) = -1

Basically, in .NET, -1 is preserved to and from numeric types, although it looks like 2^N-1 in an unsigned type of length N. this is pretty clean.

Fable2 gives answers that depend on BOTH source and result type. Notable from repl2 is:

uint64 -> int64 which seems to go via uint.
int -> uint64 negative numbers set to 0L

(-1 |> int32) -4294967296 (correct)
(-1 |> uint32) = 4294967295u (correct - and this is the most important one!)
(4294967295u |> int32) = -1 (correct, also important)
(-1L |> uint64) = 18446744069414584320uL (correct)
(18446744069414584320uL |> int64) = -4294967296L (incorrect)
(-1 |> uint64) = 0uL (incorrect)

conversions from char to int32 or uint32 go wrong in a not easy to understand way:

(-1 |> char |> uint32) = -4294967295u  (incorrect)
(-1 |> char |> int32) = -4294967296 (incorrect)

Any conversion between int64 or uint64 and some other integer is not JS standard, since 64 bit types are custom, so we should do these like .NET I think?

char does not seem to exist in JS. Characters can be converted to unicode codes which I think are normally 16 bit unsigned. So the FS conversions to char should result in positive Number values. Maybe -1 is converted to char as an all ones bit pattern which gets converted to Number as +/- 2^N-1 rather than -1 as it should be?

Most helpful comment

OK I've got a patch that satisfies the above tests, which I've added to ConvertTests.
It is not a big change to a code.

It changes semantics of conversion to/from int64/uint64 and smaller int/uint.

  • To int64/uint64: smaller ints are zero or sign extended after truncation.
  • From int64/uint64: lower bits are preserved on convert to smaller int (was not always true before)

I'll PR it.

All 8 comments

OK, here are my suggestions for tests of conversions between 64 bits and 32 bits or 64 and 64 bits. These should all be the same as .NET (I think) since JS does not have 64 bit integers.

The tests all pass in FSI

I don't see any point in documenting current status since AFAIK making these tests all pass in Fable 2 is just a matter of small adjustments to long.ts.

Test code

let prop1 = (-1 |> uint64 = 0xFFFFFFFFuL )
let prop2 = (0xFFFFFFFFu |> int64 = 0xFFFFFFFFL)
let prop3 = (-1L |> uint64 = 0xFFFFFFFFFFFFFFFFuL)
let prop4 = (0xFFFFFFFFFFFFFFFFuL |> int64 = -1L)
let prop5 = (-1L |> int32 = -1)
let prop6 = (-1L |> uint32 = 0xFFFFFFFFu)
let prop7 = (-1L |> int32 = -1)
let prop8 = (0xFFFFFFFFFFFFFFFFuL |> int32 = -1)
let prop9 = (0xFFFFFFFFFFFFFFFFuL |> uint32 = 0xFFFFFFFFu)

let check1() =
    printfn "Negative int32 sign extended to uint64=%A" prop1
    printfn "Large uint32 zero extended to int64=%A" prop2
    printfn "Negative int64 unchanged as bits to uint64=%A" prop3
    printfn "Large uint64 unchanged as bits to int64=%A" prop4
    printfn "Negative int64 unchanged to int32 = %A" prop5
    printfn "Negative int64 unchanged as lower order bits to uint32 = %A" prop6
    printfn "Negative int64 unchanged to int32 = %A" prop7
    printfn "Large uint64 unchanged as lower order bits to int32 = %A" prop8
    printfn "Large uint64 unchanged as lower order bits to uint32 = %A" prop9

Actual results (CLI Fable2.0.0-beta-002)

Renderer.fs:104 Negative int32 sign extended to uint64=false

Renderer.fs:104 Large uint32 zero extended to int64=true

Renderer.fs:104 Negative int64 unchanged as bits to uint64=true

Renderer.fs:104 Large uint64 unchanged as bits to int64=true

Renderer.fs:104 Negative int64 unchanged to int32 = true

Renderer.fs:104 Negative int64 unchanged as lower order bits to uint32 = true

Renderer.fs:104 Negative int64 unchanged to int32 = true

Renderer.fs:104 Large uint64 unchanged as lower order bits to int32 = false

Renderer.fs:104 Large uint64 unchanged as lower order bits to uint32 = false

Actual results REPL2

Negative int32 sign extended to uint64=true

String.js:124 Large uint32 zero extended to int64=true

String.js:124 Negative int64 unchanged as bits to uint64=false

String.js:124 Large uint64 unchanged as bits to int64=false

String.js:124 Negative int64 unchanged to int32 = false

String.js:124 Negative int64 unchanged as lower order bits to uint32 = true

String.js:124 Negative int64 unchanged to int32 = false

String.js:124 Large uint64 unchanged as lower order bits to int32 = false

String.js:124 Large uint64 unchanged as lower order bits to uint32 = true

NB - errors may be due to REPL2 issue with parsing of large unsigned hex literals

Expected results

All props should be true

Rationale.

In .NET (I think) smaller to larger width conversions always preserve numeric vale, except where this is impossible (negative int -> larger uint). In this case the number is sign extended.

Larger to smaller width conversions always preserve lower order bits.

@alfonsogarciacaro I'm keeping the 64 bit conversion mend issue on this thread - though it is not documentation. My own skills in Javascript are rudimentary but probably enough to mend long.js However I'm not entirely sure how to hack the compiler. I've got it running its tests. Can I just add more tests and change code till they pass running the fable-compiler project as is?

I'd also like to check with @ncave that what I propose here does not have any unintended bad consequence somewhere else, since aligning with .NET will change the semantics of the three cases above where tests fail.

@tomcl

Can I just add more tests and change code till they pass running the fable-compiler project as is?

Absolutely, that's what I do, just hack at it until it works :)
I don't see an issue with sticking to .NET integer semantics, as long as it does not result in large performance degradation, but that's just IMO.

I'll do some work on this and PR when all tests are added and working. i don't expect any real performance degradation (these conversions are anyway not that common).

Sorry, am I patching now against dev2.0 or against master? @alfonsogarciacaro

@tomcl I think patching against master if fine now as it's the branch used to deployed 2.0.0-beta-002version

OK I've got a patch that satisfies the above tests, which I've added to ConvertTests.
It is not a big change to a code.

It changes semantics of conversion to/from int64/uint64 and smaller int/uint.

  • To int64/uint64: smaller ints are zero or sign extended after truncation.
  • From int64/uint64: lower bits are preserved on convert to smaller int (was not always true before)

I'll PR it.

This is now closable! The new numbers.md documentation shows what I think now happens. If anyone finds errors these can be (I hope) fixed or else added to the documentation as caveats.

I guess there is still the possibility of getting close to .NET semantics by truncating arithmetic in all cases. Personally I'm not sure this is worth the cost in terms of more complex code and lost performance.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MangelMaxime picture MangelMaxime  路  3Comments

alfonsogarciacaro picture alfonsogarciacaro  路  3Comments

SirUppyPancakes picture SirUppyPancakes  路  3Comments

jwosty picture jwosty  路  3Comments

rommsen picture rommsen  路  3Comments