If this seems strange to anyone, it comes from the fact that all numbers in JS [1] are represented as double-precision floating point values. The bit format for doubles is a sign bit, 11 bits for the exponent, and 52 bits for the fraction.
When storing integers in floats, you use an exponent of 1, and then stuff the integer into the fraction and sign bits.
[1] I think bigint is a thing now?
Is this correct? I would have just guessed that JS stores integers as their equivalent float values, which is why above 53 bits, you stop being able to represent all whole numbers (but you can represent some whole numbers), unless JS-land has changed since I last checked in almost a decade ago.
Edit: Ignore this, I'm wrong.
const std=@import("std");
test "IEEE fp representation" {
std.debug.print("\n{x}\n", .{@bitCast(u64, @as(f64, 1.0))});
std.debug.print("\n{x}\n", .{@bitCast(u64, @as(f64, 2.0))});
}
gives 3ff0000000000000
4000000000000000
note that the fraction parts for both 1.0 and 2.0 are entirely zero, as they are powers of two times [1].00000000...The "hidden bit", while tricky and hard to understand, is probably one of the few truly good decisions that IEEE-754 comittee made out of a lot of kludgey compromises. Note that if they had done it the way you suggest, there would be degenerate representations for the same number, leading to a much smaller total possible representations for the same bitspace (this is a minor problem for most object-based "Decimal" number systems used for financial Txs -- you often need to use a specialized equality function instead of the language builtin), the "hidden one" ensures that every binary form has a uniquely represented value, not counting craziness with NaNs, and plus/minus zero, sigh.
My brain fart was forgetting how floats work, seeing that the integer-resolution range for both single and double precision floats was exactly the number of bits in the fraction + the sign bit and then pattern matching.
Set the exponent to 0 and stuff in bits for the significand. This gets you a random float in the range [1,2). Now subtract 1.0.
Note that this misses many floating point values that should be selectable if you wanted all floats between 1 and 2, but it was good enough for video games.
It's worth pointing out that IEEE 754-2008 has decimal numbers like you describe without the hidden bit. But for decimals this can be really useful because numbers "remember" their exponent. So if you're doing addition of dollar values, for example, .84 + .12 = 1.00 rather than 1 (i.e. it remembers its significant digits).
Yeah, pretty decently supported as well: https://caniuse.com/bigint
I ran into it with WASM u64/i64 values, which are also apparently decently supported at this point: https://webassembly.org/roadmap/
There are, of course, edge cases. For example, despite the fact that numbers are floats, you can still perform bit operations on them. For these operations, the numbers are implicitly converted to 32-bit ints.
JavaScript also has Typed Arrays, so you can have an array of 32-bit unsigned ints. But not a u32 itself.