Add new i128 unit tests & patch-01
7 tests fails that should pass
Proof that the current implementation has major issues
Impact / Severity
| Vulnerability | Severity |
|---|---|
| i128 Negative Arithmetic | High |
i128.fromBits 32-Bit Shift for hi2 |
High |
Incorrect clz with High Bit Set (i128) |
Medium |
| Negative Float → i128 Low Bits Zeroed | Medium |
Sign Extension & Large fromString Performance |
Low |
Patch-01 (fromBits)
1. How Sign Extension Works (i32 → i64)
Let’s say you have a 32-bit signed integer (i32) whose bits look like this:
11111111 11111111 11111111 11111110
- In 2’s complement, this bit pattern is
-2(i32). - If you directly cast this
i32toi64, the language will sign-extend the value to 64 bits. That means it copies the leftmost bit (the sign bit,1in this case) to the newly added high bits.
As a result, the 64-bit (i64) pattern becomes:
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110
^ repeated sign bits ^ original 32 bits
Thus, in decimal, this is still -2 as an i64, which might not be what you want if you needed the original bits as-is.
2. How Zero Extension Works (i32 → u32 → i64)
If your goal is to preserve the exact 32 bits without interpreting them as signed, the usual trick is:
-
Cast
i32→u32: Sinceu32is an unsigned 32-bit integer, no sign extension occurs. The bit pattern is treated as a non-negative number in 32 bits. -
Then cast
u32→i64(oru64→i64), which zero-extends to 64 bits. It fills the newly created high bits with0instead of copying the sign bit.
Following the same example (-2 in i32, which is 11111111 11111111 11111111 11111110):
-
i32 (
-2) → u32:
The same bit pattern reinterpreted as unsigned yields11111111 11111111 11111111 11111110 (decimal: 4294967294)but this time it’s considered
u32, noti32. There’s no negative concept here—just bits. -
u32 → i64:
When you cast thisu32to ani64, the language zero-extends, producing:00000000 00000000 00000000 00000000 11111111 11111111 11111111 11111110 ^ zero-extended 32 bits ^Now, numerically, this is
4294967294as ani64, which exactly preserves the original lower 32 bits without sign extension.
In other words, you get a value that strictly matches the original 32 bits but in a 64-bit container.
3. Why This Matters
- Bitwise operations / low-level code: When you’re manipulating lower 32 bits, you might not want negative values or sign bits creeping into the high portion of a 64-bit register.
- Consistency: Casting
i32tou32first forces the language to treat those 32 bits as an unsigned quantity. Any further expansion to 64 bits will keep them exactly as they appear. - Avoid unintended sign extension: Directly going
i32 → i64can lead to undesired negative values if the sign bit ofi32was1.
4. Masking Alternative: & 0xFFFFFFFF
Another common technique is using a bitmask:
let x: i32 = ...;
let extended = <i64>(x & 0xFFFFFFFF);
This manually zeros out any higher bits when interpreted as signed, effectively doing the same job as i32 → u32 → i64. Under the hood:
x & 0xFFFFFFFFchopsxdown to 32 bits in an unsigned sense.- Casting to
i64afterward prevents sign extension, because the upper 32 bits are now zero.
But conceptually, doing i32 → u32 → i64 or x & 0xFFFFFFFF → i64 is the same idea: no sign extension—just the raw 32 bits in a bigger integer.
I will create 6 more PR to fix the last 6 test that fails that I added. This PR is strictly for only patching the fromBits method.
Thanks! Can you comment out the failed tests for now and uncomment them in future PRs that fix the particular test case?
Hey, sorry, I will make the rest of the PRs as soon as possible, I am pretty busy this week but should have some time this weekend