Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Code paths that cannot accept or return DSL classes pass
| `Number` | `number` | `toI32()`, `toF64()`, `assertI32()`, `from(anytype)` |
| `String` | `string` | `toSlice(buf)`, `toOwnedSlice(alloc)`, `len()`, `from([]const u8)` |
| `Boolean` | `boolean` | `toBool()`, `assertBool()`, `from(bool)` |
| `BigInt` | `bigint` | `toI64()`, `toU64()`, `toI128()`, `from(anytype)` |
| `BigInt` | `bigint` | `toI64()`, `toU64()`, `toI64Exact()`, `toU64Exact()`, `toI128()`, `toI128LowBits()`, `from(anytype)`, `fromWords()` |
| `Date` | `Date` | `toTimestamp()`, `from(f64)` |
| `Array` | `Array` | `get(i)`, `getNumber(i)`, `length()`, `set(i, val)` |
| `Object(T)` | `object` | `get()`, `set(value)` — `T` fields must be DSL types |
Expand Down
5 changes: 5 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ describe("primitive types", () => {
expect(mod.doubleBigInt(50n)).toEqual(100n);
});

it("doubleBigInt rejects BigInts outside the i64 range", () => {
expect(() => mod.doubleBigInt(2n ** 63n)).toThrow("Overflow");
expect(() => mod.doubleBigInt(-(2n ** 63n) - 1n)).toThrow("Overflow");
});

it("doubleBigInt rejects non-bigint arguments at the JS boundary", () => {
expectTypeErrorWithMessage(() => mod.doubleBigInt(50), "Argument 1 must be a bigint");
expectTypeErrorWithMessage(() => mod.doubleBigInt("50"), "Argument 1 must be a bigint");
Expand Down
3 changes: 1 addition & 2 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ pub fn reverseString(s: String) !String {

/// Double a BigInt value.
pub fn doubleBigInt(n: BigInt) !BigInt {
var lossless: bool = false;
const val = try n.toI64(&lossless);
const val = try n.toI64Exact();
return BigInt.from(val * 2);
}

Expand Down
7 changes: 5 additions & 2 deletions src/Value.zig
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,10 @@ pub fn getValueBigintUint64(self: Value, lossless: ?*bool) NapiError!u64 {
///
/// Returns `error.Overflow` if the BigInt needs more words than `words` can hold:
/// napi sets the out `word_count` to the *required* count, which may exceed the
/// buffer, so slicing by it unchecked would read out of bounds.
/// buffer, so slicing by it unchecked would read out of bounds. napi still fills
/// `words` with the low-order words and `sign_bit` is set before the error is
/// returned, so callers that want truncation semantics may catch `error.Overflow`
/// and read the buffer directly.
///
/// NOTE: napi's C entry takes `int*` (4-byte aligned). Casting a u1 to int* is UB, since that
/// is 4-bytes aligned. We use a local `c_int` for the napi call and narrow back to `u1` for the caller.
Expand All @@ -286,9 +289,9 @@ pub fn getValueBigintWords(self: Value, sign_bit: ?*u1, words: []u64) (NapiError
try status.check(
c.napi_get_value_bigint_words(self.env, self.value, &raw_sign, &word_count, words.ptr),
);
if (word_count > words.len) return error.Overflow;
// napi guarantees raw_sign ∈ {0, 1}
if (sign_bit) |s| s.* = @intCast(raw_sign);
if (word_count > words.len) return error.Overflow;
return words[0..word_count];
}

Expand Down
60 changes: 58 additions & 2 deletions src/js/bigint.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ pub const BigInt = struct {
return self.val.getValueBigintUint64(lossless);
}

/// Converts the JavaScript BigInt exactly to a Zig `i64`.
///
/// Returns `error.Overflow` if the value is outside the `i64` range,
/// instead of the low 64 bits `toI64` returns.
pub fn toI64Exact(self: BigInt) !i64 {
var lossless = false;
const value = try self.toI64(&lossless);
if (!lossless) return error.Overflow;
return value;
}

/// Converts the JavaScript BigInt exactly to a Zig `u64`.
///
/// Returns `error.Overflow` if the value is negative or outside the `u64`
/// range, instead of the low 64 bits `toU64` returns.
pub fn toU64Exact(self: BigInt) !u64 {
var lossless = false;
const value = try self.toU64(&lossless);
if (!lossless) return error.Overflow;
return value;
}

/// Attempts to convert the JavaScript BigInt to a Zig `i128`.
///
/// This function reads the BigInt as two 64-bit words and reconstructs it
Expand All @@ -57,6 +79,24 @@ pub const BigInt = struct {
return @intCast(magnitude);
}

/// Converts the JavaScript BigInt to the low 128 bits as a Zig `i128`.
///
/// Returns `BigInt.asIntN(128, b)`
/// (https://tc39.es/ecma262/#sec-bigint.asintn), so out-of-range values
/// wrap instead of failing. Use `toI128` for an exact conversion.
pub fn toI128LowBits(self: BigInt) !i128 {
var sign_bit: u1 = 0;
var words: [2]u64 = .{ 0, 0 };
// On overflow N-API still fills the two low words and the sign.
_ = self.val.getValueBigintWords(&sign_bit, &words) catch |err| switch (err) {
error.Overflow => {},
else => |e| return e,
};
const magnitude: u128 = (@as(u128, words[1]) << 64) | words[0];
if (sign_bit == 1) return @bitCast(0 -% magnitude);
return @bitCast(magnitude);
}

/// Converts the JavaScript BigInt to a Zig `i64`, panicking on failure.
///
/// This is a convenience method for cases where the BigInt is guaranteed
Expand Down Expand Up @@ -84,8 +124,7 @@ pub const BigInt = struct {
/// Creates a JavaScript `BigInt` from a Zig integer value.
///
/// Accepts `i64`, `u64`, and `comptime_int` values within this range.
/// For `i128`/`u128` values, use `fromWords` (not currently exposed but
/// available at the `napi.Env` level).
/// For `i128`/`u128` values, use `fromWords`.
///
/// Panics if N-API operations fail (e.g., invalid environment) or for Zig
/// integer types larger than `u64` that cannot be converted to `i64`/`u64`.
Expand Down Expand Up @@ -118,6 +157,23 @@ pub const BigInt = struct {
return .{ .val = val };
}

/// Creates a JavaScript `BigInt` from a sign bit and word array.
///
/// `sign_bit` is 0 for non-negative, 1 for negative. `words` is the
/// little-endian unsigned magnitude. The resulting BigInt equals
/// `(sign_bit == 1 ? -1 : 1) * sum(words[i] << (64 * i))`.
///
/// Use this for constructing BigInts whose magnitude exceeds `u64` (e.g.,
/// i128 or larger). For `i64`/`u64` values, prefer `BigInt.from`.
///
/// Panics if N-API operations fail (e.g., invalid environment).
pub fn fromWords(sign_bit: u1, words: []const u64) BigInt {
const e = context.env();
const val = e.createBigintWords(sign_bit, words) catch
@panic("BigInt.fromWords: createBigintWords failed");
return .{ .val = val };
}

/// Returns the underlying `napi.Value` representation of this JavaScript BigInt.
pub fn toValue(self: BigInt) napi.Value {
return self.val;
Expand Down
25 changes: 18 additions & 7 deletions src/js/number.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,24 @@ pub const Number = struct {

/// Attempts to convert the JavaScript number to a Zig `i32`.
///
/// Returns an error if the conversion fails (e.g., number is too large).
/// Follows N-API int32 conversion semantics: finite values outside the
/// 32-bit range are truncated to the equivalent low 32 bits, and non-finite
/// values (`NaN`, `+Infinity`, `-Infinity`) become 0.
///
/// Returns an error if the underlying value is not a number or the N-API
/// call fails.
pub fn toI32(self: Number) !i32 {
return self.val.getValueInt32();
}

/// Attempts to convert the JavaScript number to a Zig `u32`.
///
/// Safety: The underlying napi implementation does not reject numbers out of
/// the range [0, 2^32). The caller is responsible for bounds checking.
///
/// Returns an error if the given `self.val` is not a number.
/// Follows N-API uint32 conversion semantics: finite values outside the
/// 32-bit range are truncated to the equivalent low 32 bits, and non-finite
/// values (`NaN`, `+Infinity`, `-Infinity`) become 0.
///
/// Source: https://tc39.es/ecma262/#sec-touint32
/// Returns an error if the underlying value is not a number or the N-API
/// call fails.
pub fn toU32(self: Number) !u32 {
return self.val.getValueUint32();
}
Expand Down Expand Up @@ -98,7 +103,13 @@ pub const Number = struct {

/// Attempts to convert the JavaScript number to a Zig `i64`.
///
/// Returns an error if the conversion fails (e.g., number is too large).
/// Follows N-API int64 conversion semantics: non-finite values (`NaN`,
/// `+Infinity`, `-Infinity`) become 0. Values outside JavaScript's safe
/// integer range may lose precision, and finite values outside the `i64`
/// range are clamped by Node to `minInt(i64)`/`maxInt(i64)`.
///
/// Returns an error if the underlying value is not a number or the N-API
/// call fails.
pub fn toI64(self: Number) !i64 {
return self.val.getValueInt64();
}
Expand Down
Loading