Skip to content

Throw a u32 as an unsigned int; convert u32 to number as unsigned - #355

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-throw-u32-sign
Sep 23, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-throw-u32-sign

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

throw <u32>0xFFFFFFFF caught by catch (e: number) or catch (e) gave -1. Two layers were wrong.

  • RTTI. A u32 was thrown as a signed int (.H on Windows, _ZTIi on Linux). On Windows the int's .N widening and .PEAX boxing thunks were also shared by name, so a module that throws both an s32 and a u32 used whichever thunk was built first. A u32 now has RTTI of its own:
    • Windows: .I records with their own _CT??_R0PEAX@I84.box / .eh.copy.box.I and _CT??_R0N@88.fromI / .eh.copy.num.I.
    • Linux: _ZTIj, listed by catch (e: number), bound by loadCaughtNumber with uitofp, and boxed as a u32 by an untyped catch.
  • The cast. Every u32 → number conversion lowered to sitofp, because castLLVMTypesLogic only sees the signless LLVM type. The new thunk used that cast, so the RTTI fix alone changed nothing. castTypeScriptTypes now converts an unsigned integer to a float with uitofp.

Correcting the cast exposed two more signedness bugs. In 02numbers.ts they had cancelled the first one out:

  • number → narrow int was fptosi straight to i32, which is poison above 0x7FFFFFFF. 0x80000001 << 1 gave garbage once the literal arrived as 2147483649. It now converts through i64 and truncates, which wraps modulo 2^32 the way ToInt32/ToUint32 do.
  • >>> produced a signless i32 that widened as signed. Its result is now a u32, so -1 >>> 0 is 4294967295.

Both of these, and the u32 → number cast, change behaviour outside exceptions as well.

Verification

  • New test 00throw_u32.ts (compile, jit, and the rc/none corpus). One module throws both s32 and u32. It checks typed, untyped and number catches, and that an s32 passes a u32 catch. Values are checked by sign: an == 4294967295 check passed on the broken build. On main it fails at the first assert.
  • Windows Release: 2853/2853 passed, including 02numbers.ts.
  • Linux (WSL): 2847/2847 passed.
  • DefaultLib release jit: 155/155.

🤖 Generated with Claude Code

A thrown u32 went out as a signed int (`.H` on Windows, `_ZTIi` on
Linux), and every reader took its top bit for a sign: `throw <u32>
0xFFFFFFFF` caught by `catch (e: number)` or `catch (e)` gave -1. On
Windows the int's widening and boxing thunks were also shared by name,
so a module throwing both an s32 and a u32 used whichever was built
first.

A u32 now has RTTI of its own: `.I` records with their own `.PEAX` box
and `.N` widening thunks on Windows, `_ZTIj` on Linux, listed by
`catch (e: number)`, bound by loadCaughtNumber (uitofp) and boxed as a
u32 by an untyped catch.

The thunk alone did not fix it: every u32-to-number cast lowered to
sitofp, because castLLVMTypesLogic only sees the signless LLVM type. An
unsigned integer now converts to a float with uitofp. That exposed two
more signedness gaps that had cancelled it out in 02numbers.ts:

- number to a narrow integer was fptosi straight to i32, poison above
  0x7FFFFFFF, so `0x80000001 << 1` was garbage once the literal
  arrived as 2147483649. It now goes through i64 and truncates, which
  wraps modulo 2^32 as ToInt32/ToUint32 do.
- `>>>` produced a signless i32 that widened as signed; its result is
  now a u32, so `-1 >>> 0` is 4294967295.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 563b191 into main Sep 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-throw-u32-sign branch September 23, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant