Visitar URL original
refactor!: rename Number.toU64Exact to toSafeInteger · ChainSafe/zapi@b8e72d4 · GitHub
Skip to content

Commit b8e72d4

Browse files
nazarhussainclaude
andcommitted
refactor!: rename Number.toU64Exact to toSafeInteger
toU64Exact never returns a value above 2^53-1, so the name promised a range it could not deliver -- especially sitting next to toU32Exact, which is exact across its entire nominal range. The 2^53 cap itself is correct and is kept. A JS number is an f64, and @floatFromInt(std.math.maxInt(u64)) rounds *up* to 2^64, which would admit a value @intFromFloat cannot represent. Both conversions now share a private toUnsignedExact(T) that derives the bound with @min(maxInt(T), maxInt(u53)), so the clamp is total instead of a constant repeated per overload. Also adds DSL coverage for toU32Exact, which has had none since it landed, including a case pinning where the two conversions diverge. BREAKING CHANGE: Number.toU64Exact is renamed to Number.toSafeInteger, with no deprecation alias. Behaviour is unchanged -- the same inputs are accepted and the same error.InvalidUnsignedInteger is returned -- so migrating is a rename at the call site. lodestar-z is the only consumer: 3 call sites, in bindings/napi/BeaconConfig.zig and bindings/napi/BeaconStateView.zig. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ff6e70e commit b8e72d4

3 files changed

Lines changed: 58 additions & 29 deletions

File tree

‎examples/js_dsl/mod.test.ts‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,36 @@ describe("primitive types", () => {
5959
expectTypeErrorWithMessage(() => mod.doubleNumber("21"), "Argument 1 must be a number");
6060
});
6161

62-
it("exactU64 accepts safe unsigned integers", () => {
63-
expect(mod.exactU64(0)).toEqual(0);
64-
expect(mod.exactU64(2 ** 32)).toEqual(2 ** 32);
65-
expect(mod.exactU64(Number.MAX_SAFE_INTEGER)).toEqual(Number.MAX_SAFE_INTEGER);
62+
it("exactU32 accepts every value in the u32 range", () => {
63+
expect(mod.exactU32(0)).toEqual(0);
64+
expect(mod.exactU32(2 ** 31)).toEqual(2 ** 31);
65+
expect(mod.exactU32(2 ** 32 - 1)).toEqual(2 ** 32 - 1);
6666
});
6767

68-
it("exactU64 rejects values that are not exact u64 integers", () => {
68+
it("exactU32 rejects values outside the u32 range", () => {
69+
for (const value of [-1, 1.5, 2 ** 32, Number.MAX_SAFE_INTEGER, Infinity, -Infinity, NaN]) {
70+
expect(() => mod.exactU32(value), `value ${value}`).toThrow("InvalidUnsignedInteger");
71+
}
72+
});
73+
74+
it("safeInteger accepts every non-negative JS safe integer", () => {
75+
expect(mod.safeInteger(0)).toEqual(0);
76+
expect(mod.safeInteger(2 ** 32)).toEqual(2 ** 32);
77+
expect(mod.safeInteger(Number.MAX_SAFE_INTEGER)).toEqual(Number.MAX_SAFE_INTEGER);
78+
});
79+
80+
it("safeInteger rejects values that are not non-negative safe integers", () => {
6981
for (const value of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1, Infinity, -Infinity, NaN]) {
70-
expect(() => mod.exactU64(value), `value ${value}`).toThrow("InvalidUnsignedInteger");
82+
expect(() => mod.safeInteger(value), `value ${value}`).toThrow("InvalidUnsignedInteger");
7183
}
7284
});
7385

86+
// 2^32 separates the two: in range for a safe integer, out of range for a u32.
87+
it("safeInteger and exactU32 differ exactly at the u32 boundary", () => {
88+
expect(mod.safeInteger(2 ** 32)).toEqual(2 ** 32);
89+
expect(() => mod.exactU32(2 ** 32)).toThrow("InvalidUnsignedInteger");
90+
});
91+
7492
it("toggleBool", () => {
7593
expect(mod.toggleBool(true)).toBe(false);
7694
expect(mod.toggleBool(false)).toBe(true);

‎examples/js_dsl/mod.zig‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,14 @@ pub fn largeUnsignedBoundary() Number {
7979
return Number.from(@as(u64, std.math.maxInt(i64)) + 1);
8080
}
8181

82-
/// Return a number validated as an exact u64 by `toU64Exact`.
83-
pub fn exactU64(n: Number) !Number {
84-
return Number.from(try n.toU64Exact());
82+
/// Return a number validated as an exact u32 by `toU32Exact`.
83+
pub fn exactU32(n: Number) !Number {
84+
return Number.from(try n.toU32Exact());
85+
}
86+
87+
/// Return a number validated as a JS safe integer by `toSafeInteger`.
88+
pub fn safeInteger(n: Number) !Number {
89+
return Number.from(try n.toSafeInteger());
8590
}
8691

8792
/// Negate a boolean.

‎src/js/number.zig‎

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ pub const Number = struct {
4444
return self.val.getValueUint32();
4545
}
4646

47-
/// Attempts to convert the JavaScript number to a `u32` without coercion.
48-
///
49-
/// Returns `error.InvalidUnsignedInteger` if the number is negative,
50-
/// fractional, non-finite, or greater than `std.math.maxInt(u32)`.
51-
pub fn toU32Exact(self: Number) !u32 {
52-
const max: f64 = @floatFromInt(std.math.maxInt(u32));
47+
/// Attempts to convert the JavaScript number to an unsigned `T` without
48+
/// coercion, rejecting anything a JS number cannot hold exactly.
49+
///
50+
/// The upper bound is clamped to `Number.MAX_SAFE_INTEGER` (2^53 - 1): a JS
51+
/// number is an `f64`, so no larger integer round-trips, and
52+
/// `@floatFromInt(std.math.maxInt(u64))` rounds *up* to 2^64, which would
53+
/// admit a value `@intFromFloat` cannot represent.
54+
fn toUnsignedExact(self: Number, comptime T: type) !T {
55+
const max: f64 = @floatFromInt(@min(std.math.maxInt(T), std.math.maxInt(u53)));
5356
const value = try self.toF64();
5457
if (!std.math.isFinite(value) or
5558
value < 0 or
@@ -61,25 +64,28 @@ pub const Number = struct {
6164
return @intFromFloat(value);
6265
}
6366

64-
/// Attempts to convert the JavaScript number to a `u64` without coercion.
67+
/// Attempts to convert the JavaScript number to a `u32` without coercion.
68+
///
69+
/// Exact across the whole `u32` range.
70+
///
71+
/// Returns `error.InvalidUnsignedInteger` if the number is negative,
72+
/// fractional, non-finite, or greater than `std.math.maxInt(u32)`.
73+
pub fn toU32Exact(self: Number) !u32 {
74+
return self.toUnsignedExact(u32);
75+
}
76+
77+
/// Attempts to convert the JavaScript number to a non-negative JS safe
78+
/// integer, returned as a `u64`.
6579
///
6680
/// Returns `error.InvalidUnsignedInteger` if the number is negative,
6781
/// fractional, non-finite, or greater than `Number.MAX_SAFE_INTEGER`
6882
/// (2^53 - 1).
6983
///
70-
/// Larger values cannot be represented exactly by a JS number;
71-
/// use `BigInt.toU64` for the full `u64` range.
72-
pub fn toU64Exact(self: Number) !u64 {
73-
const max: f64 = @floatFromInt(std.math.maxInt(u53));
74-
const value = try self.toF64();
75-
if (!std.math.isFinite(value) or
76-
value < 0 or
77-
value > max or
78-
@trunc(value) != value)
79-
{
80-
return error.InvalidUnsignedInteger;
81-
}
82-
return @intFromFloat(value);
84+
/// This is *not* a full `u64` conversion, and no such conversion exists for
85+
/// a JS number: values above 2^53 - 1 are already imprecise by the time
86+
/// they reach Zig. Use `BigInt.toU64` for the full `u64` range.
87+
pub fn toSafeInteger(self: Number) !u64 {
88+
return self.toUnsignedExact(u64);
8389
}
8490

8591
/// Attempts to convert the JavaScript number to a Zig `f64`.

0 commit comments

Comments
 (0)