Visitar URL original
refactor!: rename Number.toU64Exact to toSafeInteger (#81) · ChainSafe/zapi@d86588d · GitHub
Skip to content

Commit d86588d

Browse files
nazarhussainclaudespiral-ladder
authored
refactor!: rename Number.toU64Exact to toSafeInteger (#81)
Follow-up to #80 / v4.1.0. `toU64Exact` never returns a value above 2^53−1 — everything from 2^53 to 2^64−1 throws. The doc comment said so, but the name promised otherwise, and it promised it *specifically* because `toU32Exact` sits right above it and **is** exact across its entire nominal range. Two parallel names, two different contracts. ### The 2^53 cap is correct and is kept It isn't a shortcut — it's the only way to write this against an `f64`: ``` u53max as f64 = 9007199254740991 exact round-trip = true u64max as f64 = 18446744073709552000 == 2^64, rounds_up = true ``` `@floatFromInt(std.math.maxInt(u64))` rounds **up** to exactly 2^64, so a naive full-range `value > max` check would admit 2^64 itself and hand it to `@intFromFloat`, which cannot represent it. So the behaviour stays; only the name changes. ### What changed - New private `toUnsignedExact(comptime T, comptime max_int)`, shared by both public conversions — the ten-line body is no longer duplicated. Each caller states its own bound, and a `comptime` assert makes a bound above `maxInt(u53)` a compile error rather than one `@intFromFloat` cannot represent. *(Updated from an earlier `@min`-derived bound after review — see the thread on `src/js/number.zig`.)* - `toU32Exact` — behaviour unchanged, now a one-liner over the helper. - `toU64Exact` → `toSafeInteger() !u64`, with a doc comment that says plainly that no exact `u64` conversion exists for a JS number and points at `BigInt.toU64`. - Adds DSL coverage for `toU32Exact`, which has had none since it landed in #71, plus a case pinning `2**32` as the exact point where the two conversions diverge. ### Breaking change Renamed with no deprecation alias, and committed as `refactor!:` with a `BREAKING CHANGE:` footer so release-please cuts a major. Keeping a misleadingly-named alias for a whole major cycle would defeat the point of the change. **Migration is a rename at the call site** — behaviour is identical, the same inputs are accepted and the same `error.InvalidUnsignedInteger` is returned. lodestar-z is the only consumer, with 30 call sites: | File | Sites | |---|---| | `bindings/napi/BeaconStateView.zig` | 29 | | `bindings/napi/BeaconConfig.zig` | 1 | `chainConfigU64` in `BeaconConfig.zig` already wraps the conversion to map `+Infinity` to `maxInt(u64)` for disabled fork epochs; that wrapper is unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: bing <spiralladder@fastmail.com>
1 parent ff6e70e commit d86588d

3 files changed

Lines changed: 59 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: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ 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 outside `[0, max_int]` and anything a JS
49+
/// number cannot hold exactly.
50+
///
51+
/// The largest representable unsigned integer in zapi is
52+
/// bounded by `self`'s bound which is
53+
/// `Number.MAX_SAFE_INTEGER` (2^53 - 1), a u53 in Zig.
54+
fn toUnsignedExact(self: Number, comptime T: type, comptime max_int: comptime_int) !T {
55+
comptime std.debug.assert(max_int <= std.math.maxInt(u53));
56+
const max: f64 = @floatFromInt(max_int);
5357
const value = try self.toF64();
5458
if (!std.math.isFinite(value) or
5559
value < 0 or
@@ -61,25 +65,28 @@ pub const Number = struct {
6165
return @intFromFloat(value);
6266
}
6367

64-
/// Attempts to convert the JavaScript number to a `u64` without coercion.
68+
/// Attempts to convert the JavaScript number to a `u32` without coercion.
69+
///
70+
/// Exact across the whole `u32` range.
71+
///
72+
/// Returns `error.InvalidUnsignedInteger` if the number is negative,
73+
/// fractional, non-finite, or greater than `std.math.maxInt(u32)`.
74+
pub fn toU32Exact(self: Number) !u32 {
75+
return self.toUnsignedExact(u32, std.math.maxInt(u32));
76+
}
77+
78+
/// Attempts to convert the JavaScript number to a non-negative JS safe
79+
/// integer, returned as a `u64`.
6580
///
6681
/// Returns `error.InvalidUnsignedInteger` if the number is negative,
6782
/// fractional, non-finite, or greater than `Number.MAX_SAFE_INTEGER`
6883
/// (2^53 - 1).
6984
///
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);
85+
/// This is *not* a full `u64` conversion, and no such conversion exists for
86+
/// a JS number: values above 2^53 - 1 are already imprecise by the time
87+
/// they reach Zig. Use `BigInt.toU64` for the full `u64` range.
88+
pub fn toSafeInteger(self: Number) !u64 {
89+
return self.toUnsignedExact(u64, std.math.maxInt(u53));
8390
}
8491

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

0 commit comments

Comments
 (0)