diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index b348ede..73cbc99 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -59,18 +59,36 @@ describe("primitive types", () => { expectTypeErrorWithMessage(() => mod.doubleNumber("21"), "Argument 1 must be a number"); }); - it("exactU64 accepts safe unsigned integers", () => { - expect(mod.exactU64(0)).toEqual(0); - expect(mod.exactU64(2 ** 32)).toEqual(2 ** 32); - expect(mod.exactU64(Number.MAX_SAFE_INTEGER)).toEqual(Number.MAX_SAFE_INTEGER); + it("exactU32 accepts every value in the u32 range", () => { + expect(mod.exactU32(0)).toEqual(0); + expect(mod.exactU32(2 ** 31)).toEqual(2 ** 31); + expect(mod.exactU32(2 ** 32 - 1)).toEqual(2 ** 32 - 1); }); - it("exactU64 rejects values that are not exact u64 integers", () => { + it("exactU32 rejects values outside the u32 range", () => { + for (const value of [-1, 1.5, 2 ** 32, Number.MAX_SAFE_INTEGER, Infinity, -Infinity, NaN]) { + expect(() => mod.exactU32(value), `value ${value}`).toThrow("InvalidUnsignedInteger"); + } + }); + + it("safeInteger accepts every non-negative JS safe integer", () => { + expect(mod.safeInteger(0)).toEqual(0); + expect(mod.safeInteger(2 ** 32)).toEqual(2 ** 32); + expect(mod.safeInteger(Number.MAX_SAFE_INTEGER)).toEqual(Number.MAX_SAFE_INTEGER); + }); + + it("safeInteger rejects values that are not non-negative safe integers", () => { for (const value of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1, Infinity, -Infinity, NaN]) { - expect(() => mod.exactU64(value), `value ${value}`).toThrow("InvalidUnsignedInteger"); + expect(() => mod.safeInteger(value), `value ${value}`).toThrow("InvalidUnsignedInteger"); } }); + // 2^32 separates the two: in range for a safe integer, out of range for a u32. + it("safeInteger and exactU32 differ exactly at the u32 boundary", () => { + expect(mod.safeInteger(2 ** 32)).toEqual(2 ** 32); + expect(() => mod.exactU32(2 ** 32)).toThrow("InvalidUnsignedInteger"); + }); + it("toggleBool", () => { expect(mod.toggleBool(true)).toBe(false); expect(mod.toggleBool(false)).toBe(true); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index 23e928c..f01b73e 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -79,9 +79,14 @@ pub fn largeUnsignedBoundary() Number { return Number.from(@as(u64, std.math.maxInt(i64)) + 1); } -/// Return a number validated as an exact u64 by `toU64Exact`. -pub fn exactU64(n: Number) !Number { - return Number.from(try n.toU64Exact()); +/// Return a number validated as an exact u32 by `toU32Exact`. +pub fn exactU32(n: Number) !Number { + return Number.from(try n.toU32Exact()); +} + +/// Return a number validated as a JS safe integer by `toSafeInteger`. +pub fn safeInteger(n: Number) !Number { + return Number.from(try n.toSafeInteger()); } /// Negate a boolean. diff --git a/src/js/number.zig b/src/js/number.zig index eeb08dc..aca3b0b 100644 --- a/src/js/number.zig +++ b/src/js/number.zig @@ -44,12 +44,16 @@ pub const Number = struct { return self.val.getValueUint32(); } - /// Attempts to convert the JavaScript number to a `u32` without coercion. - /// - /// Returns `error.InvalidUnsignedInteger` if the number is negative, - /// fractional, non-finite, or greater than `std.math.maxInt(u32)`. - pub fn toU32Exact(self: Number) !u32 { - const max: f64 = @floatFromInt(std.math.maxInt(u32)); + /// Attempts to convert the JavaScript number to an unsigned `T` without + /// coercion, rejecting anything outside `[0, max_int]` and anything a JS + /// number cannot hold exactly. + /// + /// The largest representable unsigned integer in zapi is + /// bounded by `self`'s bound which is + /// `Number.MAX_SAFE_INTEGER` (2^53 - 1), a u53 in Zig. + fn toUnsignedExact(self: Number, comptime T: type, comptime max_int: comptime_int) !T { + comptime std.debug.assert(max_int <= std.math.maxInt(u53)); + const max: f64 = @floatFromInt(max_int); const value = try self.toF64(); if (!std.math.isFinite(value) or value < 0 or @@ -61,25 +65,28 @@ pub const Number = struct { return @intFromFloat(value); } - /// Attempts to convert the JavaScript number to a `u64` without coercion. + /// Attempts to convert the JavaScript number to a `u32` without coercion. + /// + /// Exact across the whole `u32` range. + /// + /// Returns `error.InvalidUnsignedInteger` if the number is negative, + /// fractional, non-finite, or greater than `std.math.maxInt(u32)`. + pub fn toU32Exact(self: Number) !u32 { + return self.toUnsignedExact(u32, std.math.maxInt(u32)); + } + + /// Attempts to convert the JavaScript number to a non-negative JS safe + /// integer, returned as a `u64`. /// /// Returns `error.InvalidUnsignedInteger` if the number is negative, /// fractional, non-finite, or greater than `Number.MAX_SAFE_INTEGER` /// (2^53 - 1). /// - /// Larger values cannot be represented exactly by a JS number; - /// use `BigInt.toU64` for the full `u64` range. - pub fn toU64Exact(self: Number) !u64 { - const max: f64 = @floatFromInt(std.math.maxInt(u53)); - const value = try self.toF64(); - if (!std.math.isFinite(value) or - value < 0 or - value > max or - @trunc(value) != value) - { - return error.InvalidUnsignedInteger; - } - return @intFromFloat(value); + /// This is *not* a full `u64` conversion, and no such conversion exists for + /// a JS number: values above 2^53 - 1 are already imprecise by the time + /// they reach Zig. Use `BigInt.toU64` for the full `u64` range. + pub fn toSafeInteger(self: Number) !u64 { + return self.toUnsignedExact(u64, std.math.maxInt(u53)); } /// Attempts to convert the JavaScript number to a Zig `f64`.