Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 8 additions & 3 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
47 changes: 27 additions & 20 deletions src/js/number.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Comment thread
wemeetagain marked this conversation as resolved.
const max: f64 = @floatFromInt(max_int);
const value = try self.toF64();
if (!std.math.isFinite(value) or
value < 0 or
Expand All @@ -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`.
Expand Down
Loading