Skip to content

test: property-based fuzz testing for js->zig->js roundtrip#41

Open
nazarhussain wants to merge 21 commits into
mainfrom
nh/fuzz-testing
Open

test: property-based fuzz testing for js->zig->js roundtrip#41
nazarhussain wants to merge 21 commits into
mainfrom
nh/fuzz-testing

Conversation

@nazarhussain
Copy link
Copy Markdown
Contributor

Adds a property-based fuzz harness for zapi Number, BigInt and Uint8Array converters and (vitest + fast-check, driven through a dedicated test_fuzz_numeric.node addon). The harness immediately surfaced multiple latent bugs and an API sharp edge, all addressed in this branch.

API changes

  • Safer-by-default toI64 / toU64 / toI128: these now return error.InvalidArg on out-of-range BigInts instead of silently truncating. Callers who explicitly want low-bits semantics use the new toI64LowBits / toU64LowBits / toI128LowBits variants. NAPI's lossless flag is exposed as the lossless out-pointer on the *LowBits calls.
  • New BigInt.fromWords(sign_bit, words) — DSL surface for constructing BigInts whose magnitude exceeds u64 (e.g., i128). Closes the gap the existing BigInt.from doc comment noted but couldn't fill.
  • Doc comments on Number.toI32/U32/F64/I64 updated to accurately describe N-API conversion semantics (NaN/Inf → 0, range clamping, etc.).

nazarhussain and others added 21 commits May 28, 2026 12:56
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NAPI sets word_count to the required size even when the buffer is
smaller than needed; the previous code returned words[0..word_count],
which is UB when word_count > words.len. Clamping via @min makes the
function safe and gives callers the semantics of truncate-to-low-N-bits.
Previous toI128 panicked on three input classes: zero (result[0] on
empty slice), minInt(i128) (@intcast overflow during negation), and
out-of-range magnitudes. Reworks the function to use a pre-zeroed local
buffer and wrapping arithmetic, returning BigInt.asIntN(128, b)
semantics for out-of-domain inputs.

Documents the in-domain (exact) and out-of-domain (truncation) contract
in the doc comment, and adds an explicit comment on the magnitude == 0
guard for out-of-range negatives whose low 128 bits are all zero
(e.g. -2^128n), where asIntN(128, b) === 0n.
Thin DSL wrapper over napi_create_bigint_words. Required for
round-tripping i128 and larger values through the DSL without going
through the i64/u64 path. Accepts a sign bit and a little-endian
u64-word slice; the resulting BigInt equals
(sign == 1 ? -1 : 1) * sum(words[i] << (64 * i)).
Round-trips JS BigInt → i128 → JS BigInt via toI128 and fromWords.
Property test covers the i128 boundary plus out-of-range truncation via
BigInt.asIntN(128, ·). Oracle test adds deterministic boundary cases for
zero, I128_MIN/MAX (exact), just-out-of-range ±1, and oversized values
whose low 128 bits are all zero (expected result: 0n).
Zig 0.16's `zig fmt` reorders `@alignCast(@ptrCast(x))` to
`@ptrCast(@aligncast(x))`. The pre-existing form was triggering format
churn in working trees across every `zig build` invocation; applying
the canonical order once stops the noise.
The seeds-workflow doc claimed NaN/Infinity could be persisted via a
{"__special": ...} encoding, but no corresponding reviver exists in
fuzz.test.ts — following the README literally would silently corrupt
test input. NaN/Infinity are already in edgeNumbers and exercised every
run, so persisting them adds nothing. Trim the README to match reality.
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