Skip to content

fix(js)!: make BigInt conversions exact and add a shared LossyConversion error - #83

Draft
nazarhussain wants to merge 5 commits into
mainfrom
nh/bigint-exact-conversions
Draft

nazarhussain wants to merge 5 commits into
mainfrom
nh/bigint-exact-conversions

Conversation

@nazarhussain

@nazarhussain nazarhussain commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Split out of #41 so the library changes can be reviewed separately from the fuzz harness. The fuzz testing in #41 found these issues and depends on the new APIs, so #41 now targets this branch.

Why

The old toI64(lossless) / toU64(lossless) truncated out-of-range BigInts silently unless the caller checked the optional flag. Passing null threw the signal away, so 2n ** 64n + 5n came back as 5.

  • assertI64 / assertU64 never caught overflow. They were documented to panic on failure, but they called toI64(null), which never errors for out-of-range values, so they returned truncated values. They now panic as documented.
  • The old toU64 doc was wrong. It said negative BigInts return an error. N-API actually succeeds, sets lossless = false, and returns the wrapped bits.
  • Consistent with toI128, which already rejected out-of-range values on main.
  • Intent shows at the call site: toI64LowBits(&lossless) makes deliberate truncation obvious in review.

Changes

  • fix(js)!: BigInt conversions are exact by default. toI64() and toU64() now return error.LossyConversion when the value does not fit, instead of silently returning the low bits. toI128() reports out-of-range values as error.LossyConversion (was error.Overflow).
  • New lossy variants: toI64LowBits(lossless) and toU64LowBits(lossless) keep the previous N-API truncation behaviour. toI128LowBits() gives BigInt.asIntN(128, b) semantics.
  • feat(js): BigInt.fromWords(sign_bit, words) builds BigInts wider than 64 bits, e.g. i128.
  • fix(napi): getValueBigintWords sets sign_bit before returning error.Overflow, so callers that catch the error can still read the truncated value with the correct sign.
  • refactor(js)!: one error for lossy conversions. Number.toU32Exact and Number.toSafeInteger now return error.LossyConversion (was error.InvalidUnsignedInteger), the same error as the exact BigInt conversions. A new name avoids InvalidArg, which N-API itself returns for a bad argument.
  • docs(js): Number.toI32, toU32 and toI64 doc comments now describe N-API's truncation and clamping behaviour.

error.LossyConversion

All exact integer conversions now fail with the same error when the JS value can't be represented exactly in the target Zig type:

Function Returns error.LossyConversion when Previously
BigInt.toI64() value is outside the i64 range truncated silently
BigInt.toU64() value is negative or outside the u64 range truncated silently
BigInt.toI128() value is outside [-2^127, 2^127) error.Overflow
Number.toU32Exact() number is negative, fractional, non-finite or above maxInt(u32) error.InvalidUnsignedInteger
Number.toSafeInteger() number is negative, fractional, non-finite or above Number.MAX_SAFE_INTEGER error.InvalidUnsignedInteger

The *LowBits variants never return it: they truncate by design and report precision loss through the lossless flag.

Why a new name instead of reusing an existing one:

  • InvalidArg is what N-API itself returns for a bad argument, so callers couldn't tell an N-API failure from a value that doesn't fit.
  • InvalidUnsignedInteger doesn't describe the signed conversions (toI64, toI128).
  • Overflow doesn't describe fractional or NaN numbers, and getValueBigintWords already uses it to mean "buffer too small".
  • "Lossy" matches the existing lossless flag, so the error reads as "this conversion would not be lossless".

Migration

// before
var lossless = false;
const v = try n.toI64(&lossless);

// after: exact, errors when out of range
const v = try n.toI64();
// after: keep the truncating behaviour
const v = try n.toI64LowBits(&lossless);

Code that matched error.Overflow from toI128, or error.InvalidUnsignedInteger from Number.toU32Exact / toSafeInteger, should match error.LossyConversion:

// before
const n = num.toU32Exact() catch |err| switch (err) {
    error.InvalidUnsignedInteger => return default,
    else => |e| return e,
};

// after
const n = num.toU32Exact() catch |err| switch (err) {
    error.LossyConversion => return default,
    else => |e| return e,
};

On the JS side, the thrown error message changes from InvalidUnsignedInteger to LossyConversion.

Testing

🤖 Generated with Claude Code

nazarhussain and others added 4 commits October 19, 2026 12:09
napi fills the low-order words and the sign even when the BigInt needs
more words than the buffer holds. Setting sign_bit before returning
error.Overflow lets callers catch the error and read the truncated value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BigInt.toI64 and BigInt.toU64 now take no arguments and return
error.InvalidArg when the value does not fit, instead of silently
returning the low bits. The previous lossy behaviour moves to
toI64LowBits and toU64LowBits, which keep the optional lossless flag.

BigInt.toI128 now returns error.InvalidArg (was error.Overflow) for
out-of-range values, and toI128LowBits adds BigInt.asIntN(128)
truncation semantics.

BREAKING CHANGE: BigInt.toI64(lossless) and BigInt.toU64(lossless) are
now toI64() and toU64() and reject out-of-range values. Use
toI64LowBits(lossless) / toU64LowBits(lossless) for truncation.
BigInt.toI128 reports out-of-range values as error.InvalidArg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onversion

Exact conversions now share one error. BigInt previously reused
InvalidArg, which collides with N-API's own invalid-argument status.

BREAKING CHANGE: Number.toU32Exact and Number.toSafeInteger return
error.LossyConversion instead of error.InvalidUnsignedInteger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nazarhussain
nazarhussain force-pushed the nh/bigint-exact-conversions branch from 0773d1c to 0dcc598 Compare October 19, 2026 10:31
@nazarhussain nazarhussain changed the title fix(js)!: make BigInt conversions exact by default fix(js)!: make BigInt conversions exact and add a shared LossyConversion error Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant