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.
(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?
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.
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
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
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
All props should be true
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.
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.
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.
I'll PR it.