Visitar URL original
feat(js): add exact BigInt conversions, toI128LowBits and fromWords (… · ChainSafe/zapi@b56bf2d · GitHub
Skip to content

Commit b56bf2d

Browse files
nazarhussainclaude
andauthored
feat(js): add exact BigInt conversions, toI128LowBits and fromWords (#91)
### Motivation The fuzz harness in #41 needs exact BigInt conversions and word-level construction. These are the non-breaking parts of #83, so #41 can merge without waiting for the breaking change. ### Description Adds `BigInt.toI64Exact`/`toU64Exact` (throw `Overflow` instead of returning low bits), `BigInt.toI128LowBits` (`BigInt.asIntN(128)`) and `BigInt.fromWords`. It also fixes `getValueBigintWords` leaving `sign_bit` unset on overflow, and documents N-API `Number` conversion semantics. Existing methods are unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b3c8e1b commit b56bf2d

6 files changed

Lines changed: 88 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Code paths that cannot accept or return DSL classes pass
172172
| `Number` | `number` | `toI32()`, `toF64()`, `assertI32()`, `from(anytype)` |
173173
| `String` | `string` | `toSlice(buf)`, `toOwnedSlice(alloc)`, `len()`, `from([]const u8)` |
174174
| `Boolean` | `boolean` | `toBool()`, `assertBool()`, `from(bool)` |
175-
| `BigInt` | `bigint` | `toI64()`, `toU64()`, `toI128()`, `from(anytype)` |
175+
| `BigInt` | `bigint` | `toI64()`, `toU64()`, `toI64Exact()`, `toU64Exact()`, `toI128()`, `toI128LowBits()`, `from(anytype)`, `fromWords()` |
176176
| `Date` | `Date` | `toTimestamp()`, `from(f64)` |
177177
| `Array` | `Array` | `get(i)`, `getNumber(i)`, `length()`, `set(i, val)` |
178178
| `Object(T)` | `object` | `get()`, `set(value)` — `T` fields must be DSL types |

‎examples/js_dsl/mod.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ describe("primitive types", () => {
113113
expect(mod.doubleBigInt(50n)).toEqual(100n);
114114
});
115115

116+
it("doubleBigInt rejects BigInts outside the i64 range", () => {
117+
expect(() => mod.doubleBigInt(2n ** 63n)).toThrow("Overflow");
118+
expect(() => mod.doubleBigInt(-(2n ** 63n) - 1n)).toThrow("Overflow");
119+
});
120+
116121
it("doubleBigInt rejects non-bigint arguments at the JS boundary", () => {
117122
expectTypeErrorWithMessage(() => mod.doubleBigInt(50), "Argument 1 must be a bigint");
118123
expectTypeErrorWithMessage(() => mod.doubleBigInt("50"), "Argument 1 must be a bigint");

‎examples/js_dsl/mod.zig‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ pub fn reverseString(s: String) !String {
107107

108108
/// Double a BigInt value.
109109
pub fn doubleBigInt(n: BigInt) !BigInt {
110-
var lossless: bool = false;
111-
const val = try n.toI64(&lossless);
110+
const val = try n.toI64Exact();
112111
return BigInt.from(val * 2);
113112
}
114113

‎src/Value.zig‎

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

‎src/js/bigint.zig‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ pub const BigInt = struct {
3434
return self.val.getValueBigintUint64(lossless);
3535
}
3636

37+
/// Converts the JavaScript BigInt exactly to a Zig `i64`.
38+
///
39+
/// Returns `error.Overflow` if the value is outside the `i64` range,
40+
/// instead of the low 64 bits `toI64` returns.
41+
pub fn toI64Exact(self: BigInt) !i64 {
42+
var lossless = false;
43+
const value = try self.toI64(&lossless);
44+
if (!lossless) return error.Overflow;
45+
return value;
46+
}
47+
48+
/// Converts the JavaScript BigInt exactly to a Zig `u64`.
49+
///
50+
/// Returns `error.Overflow` if the value is negative or outside the `u64`
51+
/// range, instead of the low 64 bits `toU64` returns.
52+
pub fn toU64Exact(self: BigInt) !u64 {
53+
var lossless = false;
54+
const value = try self.toU64(&lossless);
55+
if (!lossless) return error.Overflow;
56+
return value;
57+
}
58+
3759
/// Attempts to convert the JavaScript BigInt to a Zig `i128`.
3860
///
3961
/// This function reads the BigInt as two 64-bit words and reconstructs it
@@ -57,6 +79,24 @@ pub const BigInt = struct {
5779
return @intCast(magnitude);
5880
}
5981

82+
/// Converts the JavaScript BigInt to the low 128 bits as a Zig `i128`.
83+
///
84+
/// Returns `BigInt.asIntN(128, b)`
85+
/// (https://tc39.es/ecma262/#sec-bigint.asintn), so out-of-range values
86+
/// wrap instead of failing. Use `toI128` for an exact conversion.
87+
pub fn toI128LowBits(self: BigInt) !i128 {
88+
var sign_bit: u1 = 0;
89+
var words: [2]u64 = .{ 0, 0 };
90+
// On overflow N-API still fills the two low words and the sign.
91+
_ = self.val.getValueBigintWords(&sign_bit, &words) catch |err| switch (err) {
92+
error.Overflow => {},
93+
else => |e| return e,
94+
};
95+
const magnitude: u128 = (@as(u128, words[1]) << 64) | words[0];
96+
if (sign_bit == 1) return @bitCast(0 -% magnitude);
97+
return @bitCast(magnitude);
98+
}
99+
60100
/// Converts the JavaScript BigInt to a Zig `i64`, panicking on failure.
61101
///
62102
/// This is a convenience method for cases where the BigInt is guaranteed
@@ -84,8 +124,7 @@ pub const BigInt = struct {
84124
/// Creates a JavaScript `BigInt` from a Zig integer value.
85125
///
86126
/// Accepts `i64`, `u64`, and `comptime_int` values within this range.
87-
/// For `i128`/`u128` values, use `fromWords` (not currently exposed but
88-
/// available at the `napi.Env` level).
127+
/// For `i128`/`u128` values, use `fromWords`.
89128
///
90129
/// Panics if N-API operations fail (e.g., invalid environment) or for Zig
91130
/// integer types larger than `u64` that cannot be converted to `i64`/`u64`.
@@ -118,6 +157,23 @@ pub const BigInt = struct {
118157
return .{ .val = val };
119158
}
120159

160+
/// Creates a JavaScript `BigInt` from a sign bit and word array.
161+
///
162+
/// `sign_bit` is 0 for non-negative, 1 for negative. `words` is the
163+
/// little-endian unsigned magnitude. The resulting BigInt equals
164+
/// `(sign_bit == 1 ? -1 : 1) * sum(words[i] << (64 * i))`.
165+
///
166+
/// Use this for constructing BigInts whose magnitude exceeds `u64` (e.g.,
167+
/// i128 or larger). For `i64`/`u64` values, prefer `BigInt.from`.
168+
///
169+
/// Panics if N-API operations fail (e.g., invalid environment).
170+
pub fn fromWords(sign_bit: u1, words: []const u64) BigInt {
171+
const e = context.env();
172+
const val = e.createBigintWords(sign_bit, words) catch
173+
@panic("BigInt.fromWords: createBigintWords failed");
174+
return .{ .val = val };
175+
}
176+
121177
/// Returns the underlying `napi.Value` representation of this JavaScript BigInt.
122178
pub fn toValue(self: BigInt) napi.Value {
123179
return self.val;

‎src/js/number.zig‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,24 @@ pub const Number = struct {
2727

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

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

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

0 commit comments

Comments
 (0)