Skip to content

Add integer support to Rust JSG - #8072

Merged
piscisaureus merged 4 commits into
mainfrom
piscisaureus/rust-jsg-integers
Oct 28, 2026
Merged

piscisaureus merged 4 commits into
mainfrom
piscisaureus/rust-jsg-integers

Conversation

@piscisaureus

@piscisaureus piscisaureus commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Follows #8054, which made the Rust JSG value types mirror their V8
classes and left numbers as f64 plus a v8::Number handle. This adds
the integer side: the V8 integer classes as handle types, the
conversions V8 offers for them, integers made with Integer::New, and
integer return values on the fast API. With that in place, the bindings
and tests use integers wherever the value is one, so that f64 is the
exception rather than the rule.

Handle types and conversions:

  • jsg::v8::Integer, v8::Int32 and v8::Uint32 mirror the V8
    classes: Integer::new/new_from_unsigned, value() on each, casts
    to and from Number and Value, and a strict FromJS for the
    handles that accepts only a number in range (so a method parameter
    v8::Local<v8::Uint32> rejects -1 and 1.5), as
    v8::Local<v8::Number> accepts only a number.
  • Local<Value>::is_int32/is_uint32. V8 has no IsInteger, so a
    v8::Integer is a number that is one or the other, as in rusty_v8;
    Integer::New cannot make anything else.
  • Local<Value>::int32_value, uint32_value and integer_value run
    ToInt32, ToUint32 and ToInteger. They go through
    call_into_js, so what valueOf throws stays pending, a method that
    passes the error on rethrows it unchanged, and a termination is kept.

Integers to JavaScript and the fast API:

  • ToJS for u8..u32 and i8..i32 uses v8::Integer::New or
    NewFromUnsigned, as C++ JSG does, instead of going through f64
    and v8::Number::New.
  • FastApiReturn for the same six types, as V8's int32_t and
    uint32_t (kInt32/kUint32), with the narrower types widened to
    one of the two, so a #[jsg_method] returning an integer gets a fast
    callback. i64/u64 stay slow-path BigInts; a fast API return for
    them is not added.
  • FastApiArg for i8, i16, u8 and u16 as for i32 and u32.
    Integer arguments keep coming from V8 as doubles, converted by
    from_number, so the range checks and truncation are the slow path's.
    V8's integer argument types were considered and rejected: by default
    they convert with ToInt32's wrapping, and with kEnforceRangeBit
    they truncate before the range check, so 2 ** 31 - 0.5 would be an
    int32_t and -0.5 a uint32_t on the fast path while C++ JSG and
    the slow path throw a TypeError for both.

Audit of number types in the bindings and tests:

  • src/rust/api already used integers throughout, so no binding that
    a Worker can reach changes type.
  • In src/rust/jsg-test, counts, lengths, sums of bytes, ids, bounds
    and the struct fields, constants and properties that hold them are
    u32, i32, u8 or u16, as parameters and return values and in
    Option, Nullable and Vec, and the tests read them back as
    integers. f64 stays where a test is about ToNumber coercion
    ('2', null, valueOf, new Number(1)), NaN, -0, fractional
    values, Math-like functions, Float64Arrays or the Date
    millisecond value.
  • The jsg-bench fixture returns u32 from Rust and uint32_t from
    C++ for the lengths, counts and sums, and keeps f64 and double
    for the number, union, array, struct and callback cases, so both
    sides of each comparison convert the same way.
  • The compile tests and the README examples use u32 for counters and
    lengths.

The first commit is separate from the rest: the Dict tests and docs
from #7998 and the function-length tests from #8045 refer to
jsg::Number, which #8054 removed, so jsg-test does not build on
main as it is. They use f64 where the value is coerced and u32
where it is added, counted or made.

@piscisaureus
piscisaureus requested review from a team as code owners October 28, 2026 17:41
@clanky-the-clanker

clanky-the-clanker Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

💬 Approved with comments — No defects found in the integer handles, the ToInt32/ToUint32/ToInteger helpers, the integer ToJS impls or the fast API integer paths. One README wording suggestion, plus a note that the jsg-bench fixture was only checked for type consistency.

Findings: 1 info · 1 suggestion

The decision is advisory: this comment never approves, blocks or requests changes on the pull request.


📋 Code Quality: 💬 1 info

No defects found. One INFO note: the jsg-bench fixture was only checked for consistency.

Recommendation: 💬 Optional Nits

  • [INFO] Not fully reviewed for correctness: src/rust/jsg-bench/lib.rs and src/rust/jsg-bench/bench.c++. They were only checked for return-type consistency between the Rust and C++ benchmark sides.
View full review
  • Since the last review: the doc example in jsg/dict.rs changed from Dict<f64> to Dict<u32>, and the helpers in tests/dict.rs now use u32, so the clippy::float_cmp expectation is correctly gone. In tests/coercion_safety.rs, Conversion::ALL gains Int32, Uint32 and Integer variants. The pending-exception and termination tests therefore now also cover int32_value, uint32_value and integer_value.
  • Handles and helpers (v8.rs): Integer, Int32 and Uint32 follow the existing Number pattern. A value counts as an Integer if it is an int32 or a uint32, which is exactly what Integer::New and NewFromUnsigned can produce. The coercion helpers go through call_into_js and return Ok(None) when V8 leaves an exception pending, so a pending exception or a termination is never replaced.
  • FFI shims (ffi.c++/ffi.h): each shim makes a single V8 call (IsInt32, Int32Value(...).To(&result), Integer::New, and so on) and uses no C++ JSG. The declarations match the definitions. cTypeOf maps Int32/Uint32 to kInt32/kUint32.
  • Fast API (fast_api.rs, jsg-macros/utils.rs): the integer return impls only widen (i8/i16 to i32, u8/u16 to u32). The new argument impls still take f64 and go through from_number, so they get the slow path's range checks. The type lists in utils.rs match the impls.

This review checked src/rust/jsg-bench/lib.rs and src/rust/jsg-bench/bench.c++ for type consistency between the Rust and C++ sides only, not for correctness.


📝 Documentation: 💬 1 suggestion

One optional wording improvement in the JSG README

Recommendation: 💬 Optional Nits

  • [SUGGESTION] The README shortens v8::Local<'s, v8::Uint32> and v8::Local<'s, v8::Integer> to the bare marker types. Writing out the full handle type would be clearer. (src/rust/jsg/README.md:566)
View full review

The README sections on integer conversions, the integer handles and the ToInt32/ToUint32/ToInteger helpers match the code.

  • src/rust/jsg/README.md:566-567: the sentence reads "a v8::Local<'s, v8::Int32>, v8::Uint32 or v8::Integer (either)". The shorthand is understandable, but v8::Uint32 and v8::Integer on their own are marker types, not handles. Writing each one as v8::Local<'s, ...> would make the types easier to copy into a signature.

✅ All other checks passed

🔒 Security: ✅ LGTM

The coercion helpers go through call_into_js, so an exception that valueOf throws stays pending and a termination is never replaced. The FromJS impls and casts for the integer handles check is_int32/is_uint32 before value() reads the handle. On the fast path, integer arguments still arrive as doubles and go through the same range checks as on the slow path.


📖 AGENTS.md: ✅ LGTM

No AGENTS.md updates needed.


⚡ Performance: ✅ LGTM

ToJS for integers now calls Integer::New/NewFromUnsigned directly and no longer goes through f64. Methods that return integers can now get a fast callback. Each new FFI shim is a single V8 call.


Reviewed commit: 239740bc

@ask-bonk

ask-bonk Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Since last review: 0 resolved, 0 still open, 0 new.
LGTM!

Not re-run: kj-style (no author changes in their files since the last review)


Reviewed commit: 239740bc · github run

@codecov-commenter

codecov-commenter commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 122 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.38%. Comparing base (d68580d) to head (239740b).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
src/rust/jsg/v8.rs 0.00% 75 Missing ⚠️
src/rust/jsg/ffi.c++ 0.00% 40 Missing ⚠️
src/rust/jsg/fast_api.rs 0.00% 6 Missing ⚠️
src/rust/jsg/wrappable.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8072      +/-   ##
==========================================
- Coverage   69.49%   69.38%   -0.11%     
==========================================
  Files         695      696       +1     
  Lines      181564   181864     +300     
  Branches    29204    29213       +9     
==========================================
+ Hits       126170   126182      +12     
- Misses      38328    38615     +287     
- Partials    17066    17067       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@piscisaureus
piscisaureus force-pushed the piscisaureus/rust-jsg-integers branch 2 times, most recently from 668779c to 8219dd2 Compare October 28, 2026 19:15
The Dict and function-length tests, and the Dict docs, refer to
`jsg::Number`, a type that no longer exists, so `//src/rust/jsg-test`
does not build. The values a test coerces are `f64`; the ones it adds,
counts or makes are `u32`.
* `jsg::v8::Integer`, `v8::Int32` and `v8::Uint32` mirror the V8
  classes: `Integer::new`/`new_from_unsigned`, `value()` on each, casts
  to and from `Number` and `Value` through `impl_local_cast!`, and a
  strict `FromJS` for the handles that accepts only a number in range,
  as `v8::Local<v8::Number>` accepts only a number
* `Local<Value>::is_int32`/`is_uint32` (`IsInt32`/`IsUint32`); a
  `v8::Integer` is a number that is one or the other, as in rusty_v8,
  since V8 has no `IsInteger`
* `Local<Value>::int32_value`, `uint32_value` and `integer_value` run
  `ToInt32`, `ToUint32` and `ToInteger` through `call_into_js`, so what
  `valueOf` throws stays pending and a termination is kept
* `ToJS` for `u8`..`u32` and `i8`..`i32` makes the number with
  `v8::Integer::New` or `NewFromUnsigned`, as C++ JSG does, instead of
  going through `f64` and `v8::Number::New`
* `FastApiReturn` for the same six types: V8's `int32_t` and `uint32_t`
  (`FastApiType::Int32`/`Uint32`, `kInt32`/`kUint32`), the narrower
  types widened to one of the two, so a `#[jsg_method]` returning an
  integer gets a fast callback
* `FastApiArg` for `i8`, `i16`, `u8` and `u16` as for `i32` and `u32`:
  V8 passes a double and `from_number` converts it, so the range checks
  and truncation are the slow path's. V8's own integer argument types
  convert differently (`ToInt32` wrapping, or truncation before the
  range check with `kEnforceRangeBit`), so they are not used
* `jsg-macros` puts these types on its fast API lists
Counts, lengths, sums of bytes, identifiers and other integral values
in the jsg-test suite, the jsg-bench fixture, the compile tests and the
README examples are `u32`, `i32` or a narrower integer type, as
parameters and as return values, so that `f64` is left for values that
are numbers in general.

* `f64` stays where a test is about `ToNumber` coercion, `NaN`, `-0` or
  fractional values, and for `Math`-like functions and `Float64Array`.
* The jsg-bench fixture returns `u32` from Rust and `uint32_t` from C++
  for the cases that count or sum, so both sides convert the same way;
  the cases whose result is a number in general keep `f64` and `double`.
* The Dict tests and docs refer to `jsg::Number`, a type that no longer
  exists; they use `f64` where the value is coerced and `u32` where it
  is counted.
@piscisaureus
piscisaureus force-pushed the piscisaureus/rust-jsg-integers branch from 8219dd2 to 239740b Compare October 28, 2026 20:16
@piscisaureus
piscisaureus enabled auto-merge (squash) October 28, 2026 20:17
@piscisaureus
piscisaureus merged commit 47cde3f into main Oct 28, 2026
25 checks passed
@piscisaureus
piscisaureus deleted the piscisaureus/rust-jsg-integers branch October 28, 2026 20:40
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.

4 participants