diff --git a/src/rust/jsg-bench/README.md b/src/rust/jsg-bench/README.md index 928adef80ae..340d3bdaf51 100644 --- a/src/rust/jsg-bench/README.md +++ b/src/rust/jsg-bench/README.md @@ -91,8 +91,11 @@ are ignored. - Both buffer-copy implementations allocate new storage and copy the bytes. The length and sum cases read existing storage. String sizes are in UTF-16 code units; native strings are UTF-8. Ordinary numeric arrays use Rust `Vec` and C++ `kj::Array`. -- Numeric results use Rust `f64` and C++ `double`; integer inputs, storage, and arithmetic - remain integer-typed in both. The Rust callback checks and casts `Local` to `Function`. +- Integer results, such as lengths, counts and sums of bytes, use Rust `u32` and C++ `uint32_t`; + results that are numbers in general (`number`, `union`, the array and struct cases, and the + callback) use `f64` and `double`. A fast integer argument reaches Rust as a `double` and + converts with the slow path's checks, while C++ JSG receives it as a `uint32_t`, so the + `dispatch/u32` cases compare different argument conversions as well. The Rust callback checks and casts `Local` to `Function`. The C++ callback benchmark calls V8 directly, not through `jsg::Function`. - `allocate-collect/N` times the creation of N objects followed by garbage collection. Other collection benchmarks keep objects alive. `native/lifetime/N` times the creation of one diff --git a/src/rust/jsg-bench/bench.c++ b/src/rust/jsg-bench/bench.c++ index 5543c20458d..0b3080d6f03 100644 --- a/src/rust/jsg-bench/bench.c++ +++ b/src/rust/jsg-bench/bench.c++ @@ -54,9 +54,6 @@ class Node final: public Object { uint32_t getValue() { return value; } - double getJsValue() { - return value; - } void setChild(Ref node) { child = kj::mv(node); } @@ -65,7 +62,7 @@ class Node final: public Object { } JSG_RESOURCE_TYPE(Node) { - JSG_READONLY_PROTOTYPE_PROPERTY(value, getJsValue); + JSG_READONLY_PROTOTYPE_PROPERTY(value, getValue); JSG_READONLY_PROTOTYPE_PROPERTY(child, getChild); JSG_METHOD(setChild); } @@ -85,10 +82,10 @@ class Target final: public Object { void noop() {} static void staticNoop() {} - double add(uint32_t a) { + uint32_t add(uint32_t a) { return a + 1; } - double addFour(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { + uint32_t addFour(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { return a + b + c + d; } double number(double a) { @@ -100,13 +97,13 @@ class Target final: public Object { v8::Local raw(v8::Local a) { return a; } - double getValue() { + uint32_t getValue() { return value; } void setValue(uint32_t a) { value = a; } - double optional(jsg::Optional a) { + uint32_t optional(jsg::Optional a) { return a.orDefault(7); } @@ -122,7 +119,7 @@ class Target final: public Object { KJ_UNREACHABLE; } - double stringLen(kj::String a) { + uint32_t stringLen(kj::String a) { return static_cast(a.size()); } kj::String stringEcho(kj::String a) { @@ -153,10 +150,10 @@ class Target final: public Object { Octet octetMake() { return {1, 2, 3, 4, 5, 6, 7, 8}; } - double bytesLen(v8::Local a) { + uint32_t bytesLen(v8::Local a) { return static_cast(a->ByteLength()); } - double bytesCopy(v8::Local a) { + uint32_t bytesCopy(v8::Local a) { auto bytes = kj::heapArray(a->ByteLength() == 0 ? kj::ArrayPtr() : kj::arrayPtr(static_cast(a->Buffer()->Data()) + a->ByteOffset(), @@ -164,7 +161,7 @@ class Target final: public Object { benchmark::DoNotOptimize(bytes.begin()); return static_cast(bytes.size()); } - double bytesSum(v8::Local a) { + uint32_t bytesSum(v8::Local a) { if (a->ByteLength() == 0) return 0; auto bytes = kj::arrayPtr( static_cast(a->Buffer()->Data()) + a->ByteOffset(), a->ByteLength()); @@ -178,7 +175,7 @@ class Target final: public Object { Ref sameNode() { return child.addRef(); } - double takeNode(Ref a) { + uint32_t takeNode(Ref a) { return a->getValue(); } double callback(Lock& js, v8::Local a, double n) { diff --git a/src/rust/jsg-bench/lib.rs b/src/rust/jsg-bench/lib.rs index 54892037a85..5f8cba8cccb 100644 --- a/src/rust/jsg-bench/lib.rs +++ b/src/rust/jsg-bench/lib.rs @@ -80,8 +80,8 @@ impl Node { } #[jsg_property(prototype)] - fn get_value(&self) -> f64 { - self.value.get().into() + fn get_value(&self) -> u32 { + self.value.get() } #[jsg_method] @@ -112,13 +112,13 @@ impl Target { fn static_noop() {} #[jsg_method] - fn add(&self, a: u32) -> f64 { - (a + 1).into() + fn add(&self, a: u32) -> u32 { + a + 1 } #[jsg_method] - fn add_four(&self, a: u32, b: u32, c: u32, d: u32) -> f64 { - (a + b + c + d).into() + fn add_four(&self, a: u32, b: u32, c: u32, d: u32) -> u32 { + a + b + c + d } #[jsg_method] @@ -137,8 +137,8 @@ impl Target { } #[jsg_property(prototype)] - fn get_value(&self) -> f64 { - self.value.get().into() + fn get_value(&self) -> u32 { + self.value.get() } #[jsg_property(prototype)] @@ -147,8 +147,8 @@ impl Target { } #[jsg_property(instance)] - fn get_own(&self) -> f64 { - self.value.get().into() + fn get_own(&self) -> u32 { + self.value.get() } #[jsg_property(instance)] @@ -157,8 +157,8 @@ impl Target { } #[jsg_method] - fn optional(&self, a: Option) -> f64 { - a.unwrap_or(7).into() + fn optional(&self, a: Option) -> u32 { + a.unwrap_or(7) } #[jsg_method] @@ -170,8 +170,8 @@ impl Target { } #[jsg_method] - fn string_len(&self, a: String) -> f64 { - (a.len() as u32).into() + fn string_len(&self, a: String) -> u32 { + a.len() as u32 } #[jsg_method] @@ -224,24 +224,20 @@ impl Target { } #[jsg_method] - fn bytes_len(&self, a: v8::Local<'_, v8::Uint8Array>) -> f64 { - (a.byte_length() as u32).into() + fn bytes_len(&self, a: v8::Local<'_, v8::Uint8Array>) -> u32 { + a.byte_length() as u32 } #[jsg_method] - fn bytes_copy<'s>(&self, lock: &mut jsg::Lock<'s>, a: v8::Local<'s, v8::Uint8Array>) -> f64 { + fn bytes_copy<'s>(&self, lock: &mut jsg::Lock<'s>, a: v8::Local<'s, v8::Uint8Array>) -> u32 { let a = a.as_slice(lock).to_vec(); black_box(&a); - (a.len() as u32).into() + a.len() as u32 } #[jsg_method] - fn bytes_sum<'s>(&self, lock: &mut jsg::Lock<'s>, a: v8::Local<'s, v8::Uint8Array>) -> f64 { - a.as_slice(lock) - .iter() - .map(|&x| u32::from(x)) - .sum::() - .into() + fn bytes_sum<'s>(&self, lock: &mut jsg::Lock<'s>, a: v8::Local<'s, v8::Uint8Array>) -> u32 { + a.as_slice(lock).iter().map(|&x| u32::from(x)).sum() } #[jsg_method] @@ -255,8 +251,8 @@ impl Target { } #[jsg_method] - fn take_node(&self, a: jsg::Rc) -> f64 { - a.value.get().into() + fn take_node(&self, a: jsg::Rc) -> u32 { + a.value.get() } #[jsg_method] diff --git a/src/rust/jsg-compile-tests/method-receiver-is-shared.rs b/src/rust/jsg-compile-tests/method-receiver-is-shared.rs index 92339520de1..2b3ddb5e918 100644 --- a/src/rust/jsg-compile-tests/method-receiver-is-shared.rs +++ b/src/rust/jsg-compile-tests/method-receiver-is-shared.rs @@ -30,28 +30,28 @@ impl Counter { pub fn nested(self: &&Self) {} //~ ERROR a resource method takes `&self` #[jsg_method] - pub fn explicit<'a>(self: &'a Self) -> f64 { - f64::from(self.count) + pub fn explicit<'a>(self: &'a Self) -> u32 { + self.count } #[jsg_property(prototype)] - pub fn get_total(&self) -> f64 { - f64::from(self.count) + pub fn get_total(&self) -> u32 { + self.count } #[jsg_property(prototype)] - pub fn set_total(&mut self, _v: f64) { + pub fn set_total(&mut self, _v: u32) { //~^ ERROR a resource method takes `&self` } #[jsg_property(instance)] - pub fn get_current(&mut self) -> f64 { + pub fn get_current(&mut self) -> u32 { //~^ ERROR a resource method takes `&self` - f64::from(self.count) + self.count } #[jsg_property(instance)] - pub fn set_current(&mut self, _v: f64) { + pub fn set_current(&mut self, _v: u32) { //~^ ERROR a resource method takes `&self` } @@ -63,9 +63,9 @@ impl Counter { } // A rejected method is still emitted, so its callers add no errors of their own. -fn _calls(mut counter: Counter) -> f64 { +fn _calls(mut counter: Counter) -> u32 { counter.increment(); let _ = counter.get_current(); counter.consume(); - 0.0 + 0 } diff --git a/src/rust/jsg-compile-tests/ok-local-casts.rs b/src/rust/jsg-compile-tests/ok-local-casts.rs index be96e81a397..f317430c8dd 100644 --- a/src/rust/jsg-compile-tests/ok-local-casts.rs +++ b/src/rust/jsg-compile-tests/ok-local-casts.rs @@ -22,6 +22,11 @@ macro_rules! supertypes { } pub fn all() { + supertypes!(Boolean: Value); + supertypes!(Number: Value); + supertypes!(Integer: Number, Value); + supertypes!(Int32: Integer, Number, Value); + supertypes!(Uint32: Integer, Number, Value); supertypes!(Name: Value); supertypes!(String: Name, Value); supertypes!(Symbol: Name, Value); @@ -30,6 +35,7 @@ pub fn all() { supertypes!(Date: Object, Value); supertypes!(Function: Object, Value); supertypes!(Array: Object, Value); + supertypes!(Promise: Object, Value); supertypes!(ArrayBuffer: Object, Value); supertypes!(ArrayBufferView: Object, Value); supertypes!(TypedArray: ArrayBufferView, Object, Value); diff --git a/src/rust/jsg-compile-tests/ok-this-parameter.rs b/src/rust/jsg-compile-tests/ok-this-parameter.rs index 81ec03f00c0..6960a99c471 100644 --- a/src/rust/jsg-compile-tests/ok-this-parameter.rs +++ b/src/rust/jsg-compile-tests/ok-this-parameter.rs @@ -26,8 +26,8 @@ impl Counter { } #[jsg_method] - pub fn imported(this: Rc) -> f64 { - f64::from(this.count.get()) + pub fn imported(this: Rc) -> u32 { + this.count.get() } #[jsg_method] @@ -37,7 +37,7 @@ impl Counter { by: u32, ) -> jsg::Result> { this.count.set(this.count.get() + by); - f64::from(this.count.get()).to_js(lock) + this.count.get().to_js(lock) } #[jsg_method] @@ -56,8 +56,8 @@ impl Counter { } #[jsg_property] - pub fn get_total(this: jsg::Rc) -> f64 { - f64::from(this.count.get()) + pub fn get_total(this: jsg::Rc) -> u32 { + this.count.get() } #[jsg_property] @@ -66,8 +66,8 @@ impl Counter { } #[jsg_property(instance)] - pub fn get_current(this: jsg::Rc) -> f64 { - f64::from(this.count.get()) + pub fn get_current(this: jsg::Rc) -> u32 { + this.count.get() } #[jsg_inspect_property] diff --git a/src/rust/jsg-compile-tests/ok-wildcard-property-shapes.rs b/src/rust/jsg-compile-tests/ok-wildcard-property-shapes.rs index 3a71777dbfa..e35bd499ace 100644 --- a/src/rust/jsg-compile-tests/ok-wildcard-property-shapes.rs +++ b/src/rust/jsg-compile-tests/ok-wildcard-property-shapes.rs @@ -38,7 +38,7 @@ pub struct Borrowed; #[jsg_resource] impl Borrowed { #[jsg_wildcard_property] - pub fn get<'a>(self: &'a Self, name: &str) -> Option { + pub fn get<'a>(self: &'a Self, name: &str) -> Option { let _ = (self, name); None } diff --git a/src/rust/jsg-compile-tests/this-parameter-is-the-receiver.rs b/src/rust/jsg-compile-tests/this-parameter-is-the-receiver.rs index edf221d8791..eb1998bc130 100644 --- a/src/rust/jsg-compile-tests/this-parameter-is-the-receiver.rs +++ b/src/rust/jsg-compile-tests/this-parameter-is-the-receiver.rs @@ -66,14 +66,14 @@ impl Counter { } #[jsg_property] - pub fn get_total(&self, this: jsg::Rc) -> f64 { + pub fn get_total(&self, this: jsg::Rc) -> u32 { //~^ ERROR `this` must be the first parameter let _ = this; - f64::from(self.count.get()) + self.count.get() } #[jsg_property] - pub fn set_total(lock: &mut jsg::Lock, this: jsg::Rc, v: f64) { + pub fn set_total(lock: &mut jsg::Lock, this: jsg::Rc, v: u32) { //~^ ERROR `this` must be the first parameter let _ = (lock, this, v); } diff --git a/src/rust/jsg-macros/README.md b/src/rust/jsg-macros/README.md index bdb7dbf77b8..3a3113af3ad 100644 --- a/src/rust/jsg-macros/README.md +++ b/src/rust/jsg-macros/README.md @@ -28,7 +28,7 @@ setter on `Object.prototype` runs, and what it throws is passed on. ```rust #[jsg_struct] pub struct CaaRecord { - pub critical: f64, + pub critical: u8, pub tag: String, pub value: String, } @@ -310,16 +310,16 @@ use std::cell::{Cell, RefCell}; use jsg_macros::{jsg_resource, jsg_property}; #[jsg_resource] -struct Counter { value: Cell, id: RefCell, kind: String } +struct Counter { value: Cell, id: RefCell, kind: String } #[jsg_resource] impl Counter { // Prototype — read/write (setter detected from `set_` prefix) #[jsg_property(prototype)] - pub fn get_value(&self) -> f64 { self.value.get() } + pub fn get_value(&self) -> u32 { self.value.get() } #[jsg_property(prototype)] - pub fn set_value(&self, v: f64) { self.value.set(v); } + pub fn set_value(&self, v: u32) { self.value.set(v); } // Prototype — read-only (explicit `readonly`) #[jsg_property(prototype, readonly)] diff --git a/src/rust/jsg-macros/utils-test.rs b/src/rust/jsg-macros/utils-test.rs index 86a5b3d234a..c531f33bbb9 100644 --- a/src/rust/jsg-macros/utils-test.rs +++ b/src/rust/jsg-macros/utils-test.rs @@ -176,7 +176,18 @@ fn fast_api_signature_cases() { None ); assert_eq!(fast(parse_quote!(fn f(&self, x: Option))), None); - assert_eq!(fast(parse_quote!(fn f(&self) -> i32)), None); + assert_eq!(fast(parse_quote!(fn f(&self) -> i32)), Some(0)); + assert_eq!( + fast(parse_quote!(fn f(&self, x: u8, y: i16) -> u16)), + Some(2) + ); + assert_eq!( + fast(parse_quote!(fn f(&self, x: i8, y: u16) -> i8)), + Some(2) + ); + assert_eq!(fast(parse_quote!(fn f(&self) -> i64)), None); + assert_eq!(fast(parse_quote!(fn f(&self, x: u64))), None); + assert_eq!(fast(parse_quote!(fn f(&self) -> Option)), None); assert_eq!( fast(parse_quote!(fn f(&self) -> Result)), None diff --git a/src/rust/jsg-macros/utils.rs b/src/rust/jsg-macros/utils.rs index e692a9d95ab..0f6160f93c9 100644 --- a/src/rust/jsg-macros/utils.rs +++ b/src/rust/jsg-macros/utils.rs @@ -231,9 +231,9 @@ pub fn elide_named_lifetimes(ty: &syn::Type) -> proc_macro2::TokenStream { } /// Types with a `FastApiArg` impl in `jsg/fast_api.rs`. -const FAST_API_ARG_TYPES: &[&str] = &["bool", "i32", "u32", "f64"]; +const FAST_API_ARG_TYPES: &[&str] = &["bool", "i8", "i16", "i32", "u8", "u16", "u32", "f64"]; /// Types with a `FastApiReturn` impl in `jsg/fast_api.rs`, besides `()`. -const FAST_API_RETURN_TYPES: &[&str] = &["bool", "f64"]; +const FAST_API_RETURN_TYPES: &[&str] = &["bool", "i8", "i16", "i32", "u8", "u16", "u32", "f64"]; /// Returns the argument types of `sig` if V8 can call it through a fast callback. /// diff --git a/src/rust/jsg-test/tests/argument_errors.rs b/src/rust/jsg-test/tests/argument_errors.rs index dcda6125f72..d24ea45e182 100644 --- a/src/rust/jsg-test/tests/argument_errors.rs +++ b/src/rust/jsg-test/tests/argument_errors.rs @@ -34,8 +34,8 @@ struct Args {} #[jsg_resource] impl Args { #[jsg_method] - pub fn take_count(&self, count: u32) -> f64 { - f64::from(count) + pub fn take_count(&self, count: u32) -> u32 { + count } #[jsg_method] @@ -57,8 +57,8 @@ impl Args { pub fn take_renamed(&self, _count: u32) {} #[jsg_method] - pub fn take_point(&self, point: Point) -> f64 { - f64::from(point.x) + pub fn take_point(&self, point: Point) -> u32 { + point.x } #[jsg_method] diff --git a/src/rust/jsg-test/tests/arrays.rs b/src/rust/jsg-test/tests/arrays.rs index 0bc781c1446..93f591dbc9e 100644 --- a/src/rust/jsg-test/tests/arrays.rs +++ b/src/rust/jsg-test/tests/arrays.rs @@ -13,7 +13,7 @@ use jsg_macros::jsg_struct; #[jsg_struct] struct Person { pub name: String, - pub age: f64, + pub age: u32, } #[jsg_resource] @@ -22,12 +22,12 @@ struct ArrayResource; #[jsg_resource] impl ArrayResource { #[jsg_method] - pub fn sum(&self, numbers: Vec) -> f64 { + pub fn sum(&self, numbers: Vec) -> u32 { numbers.iter().copied().sum() } #[jsg_method] - pub fn sum_slice(&self, numbers: &[f64]) -> f64 { + pub fn sum_slice(&self, numbers: &[u32]) -> u32 { numbers.iter().copied().sum() } @@ -52,8 +52,8 @@ impl ArrayResource { } #[jsg_method] - pub fn filter_positive(&self, numbers: Vec) -> Vec { - numbers.into_iter().filter(|&n| n > 0.0).collect() + pub fn filter_positive(&self, numbers: Vec) -> Vec { + numbers.into_iter().filter(|&n| n > 0).collect() } #[jsg_method] @@ -93,13 +93,13 @@ impl ArrayResource { &self, lock: &mut jsg::Lock<'s>, numbers: v8::Local<'s, v8::Int32Array>, - ) -> f64 { - numbers.as_slice(lock).iter().map(|&n| f64::from(n)).sum() + ) -> i32 { + numbers.as_slice(lock).iter().sum() } #[jsg_method] pub fn filter_adults(&self, people: Vec) -> Vec { - people.into_iter().filter(|p| p.age >= 18.0).collect() + people.into_iter().filter(|p| p.age >= 18).collect() } #[jsg_method] @@ -151,8 +151,10 @@ fn resource_accepts_array_parameter() { let wrapped = resource.to_js(lock)?; ctx.set_global(lock, "arr", wrapped); - let result: f64 = ctx.eval(lock, "arr.sum([1, 2, 3, 4, 5])").unwrap(); - assert!((result - 15.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "arr.sum([1, 2, 3, 4, 5])").unwrap(), + 15 + ); let result: String = ctx .eval(lock, "arr.concatStrings(['hello', 'world'])") @@ -170,14 +172,16 @@ fn resource_accepts_slice_parameter() { let wrapped = resource.to_js(lock)?; ctx.set_global(lock, "arr", wrapped); - let result: f64 = ctx.eval(lock, "arr.sumSlice([1, 2, 3, 4, 5])").unwrap(); - assert!((result - 15.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "arr.sumSlice([1, 2, 3, 4, 5])") + .unwrap(), + 15 + ); let result: String = ctx.eval(lock, "arr.joinStrings(['a', 'b', 'c'])").unwrap(); assert_eq!(result, "a-b-c"); - let result: f64 = ctx.eval(lock, "arr.sumSlice([])").unwrap(); - assert!((result - 0.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "arr.sumSlice([])").unwrap(), 0); Ok(()) }); } @@ -196,10 +200,10 @@ fn resource_returns_array() { let result: Vec = ctx.eval(lock, "arr.splitString('a, b, c')").unwrap(); assert_eq!(result, vec!["a", "b", "c"]); - let result: Vec = ctx + let result: Vec = ctx .eval(lock, "arr.filterPositive([-1, 2, -3, 4, 0])") .unwrap(); - assert_eq!(result, vec![2.0, 4.0]); + assert_eq!(result, vec![2, 4]); Ok(()) }); } @@ -217,10 +221,10 @@ fn resource_accepts_typed_array_parameter() { .unwrap(); assert_eq!(result.as_slice(lock), &[3, 2, 1]); - let result: f64 = ctx + let result: i32 = ctx .eval(lock, "arr.sumI32(new Int32Array([-10, 20, -5]))") .unwrap(); - assert!((result - 5.0).abs() < f64::EPSILON); + assert_eq!(result, 5); let result: v8::Local = ctx .eval(lock, "arr.reverseBytes(new Uint8Array([]))") @@ -273,10 +277,10 @@ fn typed_array_parameter_rejects_wrong_type() { ctx.eval(lock, "arr.reverseBytes([1, 2, 3])"); assert!(result.is_err()); - let result: Result = ctx.eval(lock, "arr.sumI32(new Uint32Array([1, 2, 3]))"); + let result: Result = ctx.eval(lock, "arr.sumI32(new Uint32Array([1, 2, 3]))"); assert!(result.is_err()); - let result: Result = ctx.eval(lock, "arr.sumI32([1, 2, 3])"); + let result: Result = ctx.eval(lock, "arr.sumI32([1, 2, 3])"); assert!(result.is_err()); Ok(()) @@ -314,8 +318,7 @@ fn vec_to_js_creates_array() { let is_array: bool = ctx.eval(lock, "Array.isArray(arr)").unwrap(); assert!(is_array); - let length: f64 = ctx.eval(lock, "arr.length").unwrap(); - assert!((length - 2.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "arr.length").unwrap(), 2); let first: String = ctx.eval(lock, "arr[0]").unwrap(); assert_eq!(first, "hello"); @@ -491,8 +494,8 @@ fn vec_from_js_parses_array() { let strings: Vec = ctx.eval(lock, "['a', 'b', 'c']").unwrap(); assert_eq!(strings, vec!["a", "b", "c"]); - let numbers: Vec = ctx.eval(lock, "[1, 2, 3]").unwrap(); - assert_eq!(numbers, vec![1.0, 2.0, 3.0]); + let numbers: Vec = ctx.eval(lock, "[1, 2, 3]").unwrap(); + assert_eq!(numbers, vec![1, 2, 3]); Ok(()) }); } @@ -505,8 +508,7 @@ fn vec_empty_roundtrip() { let js_val = vec.to_js(lock)?; ctx.set_global(lock, "arr", js_val); - let length: f64 = ctx.eval(lock, "arr.length").unwrap(); - assert!(length.abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "arr.length").unwrap(), 0); let result: Vec = ctx.eval(lock, "arr").unwrap(); assert!(result.is_empty()); @@ -518,15 +520,20 @@ fn vec_empty_roundtrip() { fn vec_nested_arrays() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let nested = vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]; + let nested = vec![vec![1_u32, 2], vec![3, 4]]; let js_val = nested.to_js(lock)?; ctx.set_global(lock, "matrix", js_val); - let first_row_sum: f64 = ctx.eval(lock, "matrix[0][0] + matrix[0][1]").unwrap(); - assert!((first_row_sum - 3.0).abs() < f64::EPSILON); - - let second_row_sum: f64 = ctx.eval(lock, "matrix[1][0] + matrix[1][1]").unwrap(); - assert!((second_row_sum - 7.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "matrix[0][0] + matrix[0][1]") + .unwrap(), + 3 + ); + assert_eq!( + ctx.eval::(lock, "matrix[1][0] + matrix[1][1]") + .unwrap(), + 7 + ); Ok(()) }); } @@ -558,8 +565,8 @@ fn to_js_typed_array_creates_each_typed_array() { assert!(is_instance, $name); let is_array: bool = ctx.eval(lock, "Array.isArray(typed)").unwrap(); assert!(!is_array, $name); - let length: f64 = ctx.eval(lock, "typed.length").unwrap(); - assert!((length - 3.0).abs() < f64::EPSILON, $name); + let length: u32 = ctx.eval(lock, "typed.length").unwrap(); + assert_eq!(length, 3, $name); }}; } check!("Uint8Array", [1u8, 2, 255]); @@ -574,8 +581,8 @@ fn to_js_typed_array_creates_each_typed_array() { check!("BigUint64Array", [0u64, 42, u64::MAX]); // The elements are what JavaScript sees. - let val: f64 = ctx.eval(lock, "new Uint8Array([1, 2, 255])[2]").unwrap(); - assert!((val - 255.0).abs() < f64::EPSILON); + let val: u8 = ctx.eval(lock, "new Uint8Array([1, 2, 255])[2]").unwrap(); + assert_eq!(val, 255); let typed = [100i64, -200].to_js_typed_array(lock); ctx.set_global(lock, "typed", typed); let val: String = ctx.eval(lock, "String(typed[1])").unwrap(); @@ -705,8 +712,7 @@ fn typed_array_empty_roundtrip() { assert!(typed.is_empty()); ctx.set_global(lock, "arr", typed); - let length: f64 = ctx.eval(lock, "arr.length").unwrap(); - assert!(length.abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "arr.length").unwrap(), 0); let result: v8::Local = ctx.eval(lock, "arr").unwrap(); assert!(result.as_slice(lock).is_empty()); @@ -752,8 +758,7 @@ fn large_typed_array_roundtrip() { let typed = data.to_js_typed_array(lock); ctx.set_global(lock, "arr", typed); - let length: f64 = ctx.eval(lock, "arr.length").unwrap(); - assert!((length - 65536.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "arr.length").unwrap(), 65536); let result: v8::Local = ctx.eval(lock, "arr").unwrap(); assert_eq!(result.as_slice(lock), data.as_slice()); @@ -999,8 +1004,8 @@ impl SliceResource { &self, lock: &mut jsg::Lock<'s>, arr: jsg::v8::Local<'s, jsg::v8::Uint8Array>, - ) -> f64 { - arr.as_slice(lock).iter().map(|&x| f64::from(x)).sum() + ) -> u32 { + arr.as_slice(lock).iter().map(|&x| u32::from(x)).sum() } /// Accepts a `Local` directly and sums its elements via `as_slice`. @@ -1009,8 +1014,8 @@ impl SliceResource { &self, lock: &mut jsg::Lock<'s>, arr: jsg::v8::Local<'s, jsg::v8::Int32Array>, - ) -> f64 { - arr.as_slice(lock).iter().map(|&x| f64::from(x)).sum() + ) -> i32 { + arr.as_slice(lock).iter().sum() } /// Accepts a `Local` directly and sums its elements via `as_slice`. @@ -1029,13 +1034,11 @@ impl SliceResource { &self, lock: &mut jsg::Lock<'s>, mut arr: jsg::v8::Local<'s, jsg::v8::Uint8Array>, - ) -> f64 { + ) -> u32 { // SAFETY: no other reference into this buffer is live during this call; // lock borrow prevents JS from running. unsafe { arr.as_mut_slice(lock) }.fill(0xFF); - #[expect(clippy::cast_precision_loss, reason = "test arrays are small")] - let len = arr.len() as f64; - len + u32::try_from(arr.len()).unwrap() } } @@ -1047,16 +1050,16 @@ fn typed_array_as_slice_zero_copy_sum() { ctx.set_global(lock, "r", res); // Uint8Array - let result: f64 = ctx + let result: u32 = ctx .eval(lock, "r.sumU8Local(new Uint8Array([10, 20, 30]))") .unwrap(); - assert!((result - 60.0).abs() < f64::EPSILON); + assert_eq!(result, 60); // Int32Array - let result: f64 = ctx + let result: i32 = ctx .eval(lock, "r.sumI32Local(new Int32Array([-1, 0, 1]))") .unwrap(); - assert!((result - 0.0).abs() < f64::EPSILON); + assert_eq!(result, 0); // Float64Array let result: f64 = ctx @@ -1088,14 +1091,14 @@ fn typed_array_as_mut_slice_mutations_visible_in_js() { // fill_ff writes 0xFF into every element in-place. // The returned length tells us how many bytes were written. - let len: f64 = ctx + let len: u32 = ctx .eval(lock, "r.fillFf(new Uint8Array([1, 2, 3]))") .unwrap(); - assert!((len - 3.0).abs() < f64::EPSILON); + assert_eq!(len, 3); // Independently verify with a captured reference: create an array in JS, // pass it in, then read back the same object. - let check: f64 = ctx + let check: u32 = ctx .eval( lock, r" @@ -1105,7 +1108,7 @@ fn typed_array_as_mut_slice_mutations_visible_in_js() { ", ) .unwrap(); - assert!((check - 1.0).abs() < f64::EPSILON); + assert_eq!(check, 1); Ok(()) }); @@ -1199,15 +1202,15 @@ fn typed_array_from_js_local_rejects_wrong_type() { ctx.set_global(lock, "r", res); // sumU8Local expects Uint8Array — Int8Array must be rejected. - let result: Result = ctx.eval(lock, "r.sumU8Local(new Int8Array([1, 2, 3]))"); + let result: Result = ctx.eval(lock, "r.sumU8Local(new Int8Array([1, 2, 3]))"); assert!(result.is_err()); // Regular Array must also be rejected. - let result: Result = ctx.eval(lock, "r.sumU8Local([1, 2, 3])"); + let result: Result = ctx.eval(lock, "r.sumU8Local([1, 2, 3])"); assert!(result.is_err()); // sumI32Local expects Int32Array — Uint32Array must be rejected. - let result: Result = ctx.eval(lock, "r.sumI32Local(new Uint32Array([1, 2, 3]))"); + let result: Result = ctx.eval(lock, "r.sumI32Local(new Uint32Array([1, 2, 3]))"); assert!(result.is_err()); Ok(()) @@ -1329,11 +1332,11 @@ fn to_js_with_fast_jsg_struct_calls_no_prototype_setters() { let people = vec![ Person { name: "Alice".to_owned(), - age: 25.0, + age: 25, }, Person { name: "Bob".to_owned(), - age: 30.0, + age: 30, }, ]; let value = people.to_js(lock)?; @@ -1397,11 +1400,11 @@ fn struct_to_js_without_fast_jsg_struct_runs_object_prototype_setters() { let people = vec![ Person { name: "Alice".to_owned(), - age: 25.0, + age: 25, }, Person { name: "Bob".to_owned(), - age: 30.0, + age: 30, }, ]; let value = people.to_js(lock)?; @@ -1442,7 +1445,7 @@ fn struct_to_js_without_fast_jsg_struct_passes_on_what_a_setter_throws() { let person = Person { name: "Alice".to_owned(), - age: 25.0, + age: 25, }; let caught = lock.try_catch(|lock| person.to_js(lock))?; let Err(exception) = caught else { @@ -1487,7 +1490,7 @@ fn vec_to_js_stops_at_the_first_element_that_fails() { .into_iter() .map(|name| Person { name: name.to_owned(), - age: 30.0, + age: 30, }) .collect::>(); let caught = lock.try_catch(|lock| people.to_js(lock))?; diff --git a/src/rust/jsg-test/tests/buffer_types.rs b/src/rust/jsg-test/tests/buffer_types.rs index 4fa3e22e50d..e6089d1f72b 100644 --- a/src/rust/jsg-test/tests/buffer_types.rs +++ b/src/rust/jsg-test/tests/buffer_types.rs @@ -28,15 +28,11 @@ fn array_buffer_new_copies_data() { let is_ab: bool = ctx.eval(lock, "buf instanceof ArrayBuffer").unwrap(); assert!(is_ab); - let len: f64 = ctx.eval(lock, "buf.byteLength").unwrap(); - assert!((len - 4.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "buf.byteLength").unwrap(), 4); // Values must be the originals, not 0xFF. - let v0: f64 = ctx.eval(lock, "new Uint8Array(buf)[0]").unwrap(); - assert!((v0 - 10.0).abs() < f64::EPSILON); - - let v3: f64 = ctx.eval(lock, "new Uint8Array(buf)[3]").unwrap(); - assert!((v3 - 40.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "new Uint8Array(buf)[0]").unwrap(), 10); + assert_eq!(ctx.eval::(lock, "new Uint8Array(buf)[3]").unwrap(), 40); Ok(()) }); @@ -49,8 +45,7 @@ fn array_buffer_new_zeroed_is_zeroed() { let buf = v8::ArrayBuffer::new_zeroed(lock, 8); ctx.set_global(lock, "buf", buf); - let len: f64 = ctx.eval(lock, "buf.byteLength").unwrap(); - assert!((len - 8.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "buf.byteLength").unwrap(), 8); // V8 zero-initializes new backing stores let all_zero: bool = ctx @@ -190,8 +185,7 @@ fn array_buffer_as_mut_slice_mutations_visible_in_js() { unsafe { buf.as_mut_slice(lock) }.copy_from_slice(&[10, 20, 30]); ctx.set_global(lock, "buf", buf); - let v1: f64 = ctx.eval(lock, "new Uint8Array(buf)[1]").unwrap(); - assert!((v1 - 20.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "new Uint8Array(buf)[1]").unwrap(), 20); Ok(()) }); @@ -287,8 +281,7 @@ fn array_buffer_into_object() { let obj: v8::Local = buf.into(); // ArrayBuffer is an Object — verify we can read byteLength as a property let bl = obj.get(lock, "byteLength").unwrap(); - let n: f64 = ::from_js(lock, bl)?; - assert!((n - 1.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, bl)?, 1); Ok(()) }); } @@ -307,10 +300,8 @@ fn array_buffer_from_js_rejects_typed_array() { #[jsg_resource] impl AbResource { #[jsg_method] - pub fn byte_len(&self, buf: jsg::v8::Local) -> f64 { - #[expect(clippy::cast_precision_loss, reason = "test buffers are small")] - let len = buf.byte_length() as f64; - len + pub fn byte_len(&self, buf: jsg::v8::Local) -> u32 { + u32::try_from(buf.byte_length()).unwrap() } } @@ -319,10 +310,13 @@ fn array_buffer_from_js_rejects_typed_array() { let res = jsg::Rc::new(AbResource); ctx.set_global(lock, "r", res); - let ok: f64 = ctx.eval(lock, "r.byteLen(new ArrayBuffer(5))").unwrap(); - assert!((ok - 5.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "r.byteLen(new ArrayBuffer(5))") + .unwrap(), + 5 + ); - let err: Result = ctx.eval(lock, "r.byteLen(new Uint8Array([1,2,3]))"); + let err: Result = ctx.eval(lock, "r.byteLen(new Uint8Array([1,2,3]))"); assert!(err.is_err()); Ok(()) @@ -393,8 +387,7 @@ fn backing_store_as_mut_slice_mutations_visible_via_local() { unsafe { bs.as_mut_slice(lock) }.copy_from_slice(&[7, 8, 9]); // The Local shares the same backing store ctx.set_global(lock, "buf", buf); - let v: f64 = ctx.eval(lock, "new Uint8Array(buf)[0]").unwrap(); - assert!((v - 7.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "new Uint8Array(buf)[0]").unwrap(), 7); Ok(()) }); } @@ -468,8 +461,7 @@ fn array_buffer_view_as_mut_slice() { // SAFETY: no other reference into this buffer is live during this call. unsafe { view.as_mut_slice(lock) }.fill(0xFF); ctx.set_global(lock, "arr", view); - let v: f64 = ctx.eval(lock, "arr[0]").unwrap(); - assert!((v - 255.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "arr[0]").unwrap(), 255); Ok(()) }); } @@ -530,8 +522,7 @@ fn array_buffer_view_into_object() { let view: v8::Local = val.try_into().unwrap(); let obj: v8::Local = view.into(); let bl = obj.get(lock, "byteLength").unwrap(); - let n: f64 = ::from_js(lock, bl)?; - assert!((n - 1.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, bl)?, 1); Ok(()) }); } @@ -556,10 +547,8 @@ fn array_buffer_view_from_js_rejects_array_buffer() { #[jsg_resource] impl ViewResource { #[jsg_method] - pub fn byte_len(&self, view: jsg::v8::Local) -> f64 { - #[expect(clippy::cast_precision_loss, reason = "test buffers are small")] - let len = view.byte_length() as f64; - len + pub fn byte_len(&self, view: jsg::v8::Local) -> u32 { + u32::try_from(view.byte_length()).unwrap() } } @@ -568,13 +557,14 @@ fn array_buffer_view_from_js_rejects_array_buffer() { let res = jsg::Rc::new(ViewResource); ctx.set_global(lock, "r", res); - let ok: f64 = ctx - .eval(lock, "r.byteLen(new Uint8Array([1,2,3]))") - .unwrap(); - assert!((ok - 3.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "r.byteLen(new Uint8Array([1,2,3]))") + .unwrap(), + 3 + ); // Plain ArrayBuffer is not an ArrayBufferView - let err: Result = ctx.eval(lock, "r.byteLen(new ArrayBuffer(3))"); + let err: Result = ctx.eval(lock, "r.byteLen(new ArrayBuffer(3))"); assert!(err.is_err()); Ok(()) diff --git a/src/rust/jsg-test/tests/coercion_safety.rs b/src/rust/jsg-test/tests/coercion_safety.rs index d79c9ea729c..08b0620da8b 100644 --- a/src/rust/jsg-test/tests/coercion_safety.rs +++ b/src/rust/jsg-test/tests/coercion_safety.rs @@ -281,8 +281,8 @@ impl ReentrantResource { } #[jsg_method] - pub fn call_count(&self) -> f64 { - f64::from(self.call_count.get()) + pub fn call_count(&self) -> u32 { + self.call_count.get() } /// Unconditionally panics, to test that a panic triggered *while V8 is @@ -316,8 +316,7 @@ fn string_from_tostring_side_effects_are_observed() { let s = ::from_js(lock, value).expect("coercion should succeed"); assert_eq!(s, "computed-1"); - let side_effect: f64 = ctx.eval(lock, "globalThis.__sideEffect").unwrap(); - assert!((side_effect - 1.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "globalThis.__sideEffect").unwrap(), 1); Ok(()) }); } @@ -342,8 +341,7 @@ fn number_from_valueof_side_effects_are_observed() { let n = ::from_js(lock, value).expect("coercion should succeed"); assert!((n - 42.0).abs() < f64::EPSILON); - let side_effect: f64 = ctx.eval(lock, "globalThis.__sideEffect").unwrap(); - assert!((side_effect - 1.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "globalThis.__sideEffect").unwrap(), 1); Ok(()) }); } @@ -374,8 +372,7 @@ fn string_coercion_reentrant_into_rust_resource_method() { let s = ::from_js(lock, value).expect("nested coercion should succeed"); assert_eq!(s, "[inner]"); - let count: f64 = ctx.eval(lock, "resource.callCount()").unwrap(); - assert!((count - 1.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "resource.callCount()").unwrap(), 1); Ok(()) }); } @@ -403,8 +400,7 @@ fn number_coercion_reentrant_into_rust_resource_method() { let n = ::from_js(lock, value).expect("nested coercion should succeed"); assert!((n - 42.0).abs() < f64::EPSILON); - let count: f64 = ctx.eval(lock, "resource.callCount()").unwrap(); - assert!((count - 1.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "resource.callCount()").unwrap(), 1); Ok(()) }); } @@ -592,16 +588,16 @@ fn object_set_from_throwing_setter_returns_err() { .eval_raw(lock, expr) .expect("expression should evaluate"); let obj: v8::Local<'_, v8::Object> = value.try_into().unwrap(); - let one = 1.0_f64.to_js(lock)?; + let one = 1_u32.to_js(lock)?; let caught = thrown_by(lock, |lock| obj.set(lock, "x", one)); assert!(is_thrown(lock, ctx, caught), "{expr}"); } let obj: v8::Local<'_, v8::Object> = ctx.eval_raw(lock, "({})").unwrap().try_into().unwrap(); - let one = 1.0_f64.to_js(lock)?; + let one = 1_u32.to_js(lock)?; obj.set(lock, "x", one)?; let x = obj.get(lock, "x")?; - assert!((f64::from_js(lock, x)? - 1.0).abs() < f64::EPSILON); + assert_eq!(u32::from_js(lock, x)?, 1); Ok(()) }); } @@ -622,12 +618,12 @@ fn array_set_from_throwing_setter_returns_err() { .eval_raw(lock, expr) .expect("expression should evaluate"); let array: v8::Local<'_, v8::Array> = value.try_into().unwrap(); - let one = 1.0_f64.to_js(lock)?; + let one = 1_u32.to_js(lock)?; let caught = thrown_by(lock, |lock| array.set(lock, 0, one)); assert!(is_thrown(lock, ctx, caught), "{expr}"); } let array: v8::Local<'_, v8::Array> = ctx.eval_raw(lock, "[]").unwrap().try_into().unwrap(); - let one = 1.0_f64.to_js(lock)?; + let one = 1_u32.to_js(lock)?; array.set(lock, 2, one)?; assert_eq!(array.len(), 3); Ok(()) @@ -667,6 +663,9 @@ fn coercion_exception_propagates_through_method() { enum Conversion { String, Number, + Int32, + Uint32, + Integer, Get, Has, Iterate, @@ -678,9 +677,12 @@ enum Conversion { } impl Conversion { - const ALL: [Self; 10] = [ + const ALL: [Self; 13] = [ Self::String, Self::Number, + Self::Int32, + Self::Uint32, + Self::Integer, Self::Get, Self::Has, Self::Iterate, @@ -696,6 +698,9 @@ impl Conversion { match self { Self::String => String::from_js(lock, value).map(drop), Self::Number => f64::from_js(lock, value).map(drop), + Self::Int32 => value.int32_value(lock).map(drop), + Self::Uint32 => value.uint32_value(lock).map(drop), + Self::Integer => value.integer_value(lock).map(drop), Self::Get => v8::Local::::try_from(value) .unwrap() .get(lock, "x") @@ -704,7 +709,7 @@ impl Conversion { .unwrap() .has(lock, "x") .map(drop), - Self::Iterate => Vec::::from_js(lock, value).map(drop), + Self::Iterate => Vec::::from_js(lock, value).map(drop), Self::Set => { let undefined = lock.undefined(); v8::Local::::try_from(value) @@ -727,7 +732,9 @@ impl Conversion { fn value_calling(self, f: &str) -> String { match self { Self::String => format!("({{ toString: {f} }})"), - Self::Number => format!("({{ valueOf: {f} }})"), + Self::Number | Self::Int32 | Self::Uint32 | Self::Integer => { + format!("({{ valueOf: {f} }})") + } Self::Get | Self::DictValue => format!("({{ get x() {{ return ({f})(); }} }})"), Self::Has => format!("new Proxy({{}}, {{ has: {f} }})"), Self::Iterate => { diff --git a/src/rust/jsg-test/tests/collections_gc.rs b/src/rust/jsg-test/tests/collections_gc.rs index 5c17f11a2ff..d62405e9b10 100644 --- a/src/rust/jsg-test/tests/collections_gc.rs +++ b/src/rust/jsg-test/tests/collections_gc.rs @@ -75,8 +75,8 @@ impl Drop for Leaf { #[jsg_resource] impl Leaf { #[jsg_method] - fn get_value(&self) -> f64 { - f64::from(self.value) + fn get_value(&self) -> u32 { + self.value } } diff --git a/src/rust/jsg-test/tests/dict.rs b/src/rust/jsg-test/tests/dict.rs index c62d8456779..22f1e89e731 100644 --- a/src/rust/jsg-test/tests/dict.rs +++ b/src/rust/jsg-test/tests/dict.rs @@ -37,25 +37,25 @@ impl Records { } #[jsg_method] - pub fn count_present(&self, dict: Dict>) -> f64 { - f64::from(u32::try_from(dict.into_iter().filter(|(_, v)| v.is_some()).count()).unwrap()) + pub fn count_present(&self, dict: Dict>) -> u32 { + u32::try_from(dict.into_iter().filter(|(_, v)| v.is_some()).count()).unwrap() } #[jsg_method] - pub fn total_lengths(&self, dict: Dict>) -> f64 { - f64::from(u32::try_from(dict.into_iter().map(|(_, v)| v.len()).sum::()).unwrap()) + pub fn total_lengths(&self, dict: Dict>) -> u32 { + u32::try_from(dict.into_iter().map(|(_, v)| v.len()).sum::()).unwrap() } #[jsg_method] - pub fn sum_nested(&self, dict: Dict>) -> f64 { + pub fn sum_nested(&self, dict: Dict>) -> u32 { dict.into_iter() .flat_map(|(_, inner)| inner.into_iter().map(|(_, v)| v)) .sum() } #[jsg_method] - pub fn count_functions<'s>(&self, dict: Dict>) -> f64 { - f64::from(u32::try_from(dict.into_iter().filter(|(_, v)| v.is_function()).count()).unwrap()) + pub fn count_functions<'s>(&self, dict: Dict>) -> u32 { + u32::try_from(dict.into_iter().filter(|(_, v)| v.is_function()).count()).unwrap() } #[jsg_method] @@ -64,16 +64,13 @@ impl Records { } #[jsg_method] - pub fn make(&self, keys: Vec) -> Dict { - keys.into_iter() - .zip(1u32..) - .map(|(key, n)| (key, f64::from(n))) - .collect() + pub fn make(&self, keys: Vec) -> Dict { + keys.into_iter().zip(1..).collect() } /// Is terminated and then returns a `Dict`, which is converted after the termination. #[jsg_method] - pub fn terminate_then_make(&self, lock: &mut jsg::Lock<'_>) -> Dict { + pub fn terminate_then_make(&self, lock: &mut jsg::Lock<'_>) -> Dict { crate::Harness::terminate_and_deliver(lock); self.make(vec!["a".to_owned()]) } @@ -431,10 +428,6 @@ fn dicts_round_trip() { } #[test] -#[expect( - clippy::float_cmp, - reason = "the values are small integers, exactly representable" -)] fn get_by_name_and_keys() { with_records(|lock, ctx| { let value = ctx @@ -448,11 +441,11 @@ fn get_by_name_and_keys() { .collect(); assert_eq!(names, ["b", "a"]); let value = object.get_by_name(lock, keys[1].into()).unwrap(); - assert_eq!(f64::from_js(lock, value).unwrap(), 3.0); + assert_eq!(u32::from_js(lock, value).unwrap(), 3); let symbol = ctx.eval_raw(lock, "Symbol.for('s')").unwrap(); let symbol = v8::Local::::try_from(symbol).unwrap(); let value = object.get_by_name(lock, symbol.into()).unwrap(); - assert_eq!(f64::from_js(lock, value).unwrap(), 2.0); + assert_eq!(u32::from_js(lock, value).unwrap(), 2); }); } diff --git a/src/rust/jsg-test/tests/eval.rs b/src/rust/jsg-test/tests/eval.rs index 45bd8154372..8e260d361f5 100644 --- a/src/rust/jsg-test/tests/eval.rs +++ b/src/rust/jsg-test/tests/eval.rs @@ -25,8 +25,7 @@ fn run_in_context_resumes_panic_and_stays_usable() { Some(&"intentional panic in run_in_context") ); harness.run_in_context(|lock, ctx| { - let result: f64 = ctx.eval(lock, "1 + 1").unwrap(); - assert!((result - 2.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "1 + 1").unwrap(), 2); Ok(()) }); } @@ -55,8 +54,7 @@ fn eval_string_concatenation() { fn eval_number_returns_number_type() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let result: f64 = ctx.eval(lock, "42").unwrap(); - assert!((result - 42.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "42").unwrap(), 42); Ok(()) }); } @@ -65,8 +63,7 @@ fn eval_number_returns_number_type() { fn eval_arithmetic_expression() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let result: f64 = ctx.eval(lock, "1 + 2 + 3").unwrap(); - assert!((result - 6.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "1 + 2 + 3").unwrap(), 6); Ok(()) }); } @@ -319,7 +316,7 @@ fn lenient_string_number() { fn lenient_array_top_level_mismatch_is_unconvertable() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let result: jsg::Lenient> = ctx.eval(lock, "42").unwrap(); + let result: jsg::Lenient> = ctx.eval(lock, "42").unwrap(); assert!(matches!(result, jsg::Lenient::Unconvertable(_))); Ok(()) }); @@ -329,7 +326,7 @@ fn lenient_array_top_level_mismatch_is_unconvertable() { fn lenient_array_nested_conversion_error_propagates() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let result = ctx.eval::>>(lock, "[Symbol()]"); + let result = ctx.eval::>>(lock, "[Symbol()]"); assert!(result.is_err()); Ok(()) }); diff --git a/src/rust/jsg-test/tests/fast_api.rs b/src/rust/jsg-test/tests/fast_api.rs index ae337b4be0d..a55ca32817b 100644 --- a/src/rust/jsg-test/tests/fast_api.rs +++ b/src/rust/jsg-test/tests/fast_api.rs @@ -19,18 +19,18 @@ struct FastResource { #[jsg_resource] impl FastResource { #[jsg_method] - pub fn add(&self, a: i32, b: i32) -> f64 { - f64::from(self.base) + f64::from(a) + f64::from(b) + pub fn add(&self, a: i32, b: i32) -> i32 { + self.base + a + b } #[jsg_method] - pub fn id_i32(&self, x: i32) -> f64 { - f64::from(x) + pub fn id_i32(&self, x: i32) -> i32 { + x } #[jsg_method] - pub fn id_u32(&self, x: u32) -> f64 { - f64::from(x) + pub fn id_u32(&self, x: u32) -> u32 { + x } #[jsg_method] @@ -43,20 +43,54 @@ impl FastResource { x } + #[jsg_method] + pub fn id_u8(&self, x: u8) -> u8 { + x + } + + #[jsg_method] + pub fn id_i8(&self, x: i8) -> i8 { + x + } + + #[jsg_method] + pub fn id_u16(&self, x: u16) -> u16 { + x + } + + #[jsg_method] + pub fn id_i16(&self, x: i16) -> i16 { + x + } + + /// Saturates, so that the boundary values of `i32` come out of a fast callback. + #[jsg_method] + #[expect(clippy::cast_possible_truncation, reason = "saturation is the point")] + pub fn to_i32(&self, x: f64) -> i32 { + x as i32 + } + + /// Saturates, so that the boundary values of `u32` come out of a fast callback. + #[jsg_method] + #[expect(clippy::cast_possible_truncation, reason = "saturation is the point")] + pub fn to_u32(&self, x: f64) -> u32 { + u32::try_from(x as i64).unwrap_or(if x < 0.0 { 0 } else { u32::MAX }) + } + #[jsg_method] pub fn touch(&self) { self.touched.set(self.touched.get() + 1); } #[jsg_method] - pub fn touched(&self) -> f64 { - f64::from(self.touched.get()) + pub fn touched(&self) -> u32 { + self.touched.get() } #[jsg_method] - pub fn panic_on(&self, code: i32) -> f64 { - assert!(code <= 0, "intentional panic in a fast callback"); - f64::from(code) + pub fn panic_on(&self, code: u32) -> u32 { + assert!(code == 0, "intentional panic in a fast callback"); + code } #[jsg_method] @@ -117,13 +151,13 @@ fn function_lengths_follow_compat_flag_with_fast_api_enabled() { .unwrap(); assert_eq!(lengths, expected); let (result, fast_calls) = count_fast_calls(|| { - ctx.eval::( + ctx.eval::( lock, &optimized("a, b", "return r.add(a, b);", "1, 2", "3, 4"), ) .unwrap() }); - assert!((result.value() - 7.0).abs() < f64::EPSILON); + assert_eq!(result, 7); assert_ne!(fast_calls, Some(0)); Ok(()) }); @@ -136,13 +170,13 @@ fn instance_method_takes_fast_path() { harness.run_in_context(|lock, ctx| { set_resource(lock, ctx, 100); let (result, fast_calls) = count_fast_calls(|| { - ctx.eval::( + ctx.eval::( lock, &optimized("a, b", "return r.add(a, b);", "1, 2", "3, 4"), ) .unwrap() }); - assert!((result - 107.0).abs() < f64::EPSILON); + assert_eq!(result, 107); assert_ne!(fast_calls, Some(0)); Ok(()) }); @@ -154,13 +188,13 @@ fn void_method_takes_fast_path() { harness.run_in_context(|lock, ctx| { set_resource(lock, ctx, 0); let (touched, fast_calls) = count_fast_calls(|| { - ctx.eval::( + ctx.eval::( lock, &format!("{}; r.touched()", optimized("", "r.touch();", "", "")), ) .unwrap() }); - assert!((touched - 2.0).abs() < f64::EPSILON); + assert_eq!(touched, 2); assert_ne!(fast_calls, Some(0)); Ok(()) }); @@ -190,13 +224,13 @@ fn no_fast_path_unless_enabled() { harness.run_in_context(|lock, ctx| { set_resource(lock, ctx, 100); let (result, fast_calls) = count_fast_calls(|| { - ctx.eval::( + ctx.eval::( lock, &optimized("a, b", "return r.add(a, b);", "1, 2", "3, 4"), ) .unwrap() }); - assert!((result - 107.0).abs() < f64::EPSILON); + assert_eq!(result, 107); assert_eq!(fast_calls.unwrap_or(0), 0); Ok(()) }); @@ -225,6 +259,23 @@ fn fast_and_slow_paths_agree() { ("idU32", "1", "-1", true), ("idU32", "1", "-0.5", true), ("idU32", "1", "NaN", true), + ("idU8", "1", "255", true), + ("idU8", "1", "255.5", true), + ("idU8", "1", "256", true), + ("idU8", "1", "-1", true), + ("idU8", "1", "-0.5", true), + ("idU8", "1", "NaN", true), + ("idU8", "1", "'7'", false), + ("idI16", "1", "-32768", true), + ("idI16", "1", "-32769", true), + ("idI16", "1", "1.9", true), + ("idI16", "1", "Infinity", true), + ("idI8", "1", "-128", true), + ("idI8", "1", "-128.5", true), + ("idI8", "1", "128", true), + ("idU16", "1", "65535", true), + ("idU16", "1", "65536", true), + ("idU16", "1", "-1", true), ("idF64", "0.5", "-2.25", true), ("idF64", "0.5", "NaN", true), ("idF64", "0.5", "-Infinity", true), @@ -269,6 +320,51 @@ fn fast_and_slow_paths_agree() { } } +/// An integer comes out of a fast callback as V8's `int32_t` or `uint32_t`, which makes the +/// same number as the slow path does, over the whole range of the type: `i32::MIN`, `u32::MAX` +/// and the other values beyond the Smi range included. +#[test] +fn integer_returns_take_fast_path() { + let cases = [ + ("toI32", "-(2 ** 31)", "-2147483648"), + ("toI32", "2 ** 31 - 1", "2147483647"), + ("toI32", "2 ** 40", "2147483647"), + ("toI32", "-7.9", "-7"), + ("toU32", "2 ** 32 - 1", "4294967295"), + ("toU32", "2 ** 31", "2147483648"), + ("toU32", "-5", "0"), + ("idU8", "255", "255"), + ("idU8", "0", "0"), + ("idI16", "-32768", "-32768"), + ("idI16", "32767", "32767"), + ("idI8", "-128", "-128"), + ("idI8", "127", "127"), + ("idU16", "65535", "65535"), + ("idU16", "0", "0"), + ]; + for (method, arg, expected) in cases { + let harness = crate::Harness::with_fast_api(); + harness.run_in_context(|lock, ctx| { + set_resource(lock, ctx, 0); + let (result, fast_calls) = count_fast_calls(|| { + ctx.eval::( + lock, + &optimized( + "x", + &format!("const v = r.{method}(x); return typeof v + ' ' + String(v);"), + "1", + arg, + ), + ) + .unwrap() + }); + assert_eq!(result, format!("number {expected}"), "{method}({arg})"); + assert_ne!(fast_calls, Some(0), "{method}({arg})"); + Ok(()) + }); + } +} + /// A fast callback passes an `f64` through as V8's `double`, so `-0` keeps its sign. #[test] fn f64_keeps_negative_zero_on_the_fast_path() { @@ -296,7 +392,7 @@ fn wrong_receiver_takes_slow_path_and_throws() { harness.run_in_context(|lock, ctx| { set_resource(lock, ctx, 0); let (err, fast_calls) = count_fast_calls(|| { - ctx.eval::( + ctx.eval::( lock, &optimized( "o", @@ -325,7 +421,7 @@ fn panic_in_fast_callback_is_an_internal_error() { harness.run_in_context(|lock, ctx| { set_resource(lock, ctx, 0); let (err, fast_calls) = count_fast_calls(|| { - ctx.eval::( + ctx.eval::( lock, &optimized("code", "return r.panicOn(code);", "0", "1"), ) diff --git a/src/rust/jsg-test/tests/function.rs b/src/rust/jsg-test/tests/function.rs index e5a9ff056d6..362910c0904 100644 --- a/src/rust/jsg-test/tests/function.rs +++ b/src/rust/jsg-test/tests/function.rs @@ -81,8 +81,8 @@ fn call_zero_args() { harness.run_in_context(|lock, ctx| { let value = ctx.eval_raw(lock, "(() => 42)").unwrap(); let func = v8::Local::::try_from(value).unwrap(); - let result = func.call::(lock, None::>, &[])?; - assert!((result - 42.0).abs() < f64::EPSILON); + let result = func.call::(lock, None::>, &[])?; + assert_eq!(result, 42); Ok(()) }); } @@ -95,10 +95,10 @@ fn call_with_args() { let value = ctx.eval_raw(lock, "((a, b) => a - b)").unwrap(); let func = v8::Local::::try_from(value).unwrap(); - let a = 10.0_f64.to_js(lock)?; - let b = 32.0_f64.to_js(lock)?; - let result = func.call::(lock, None::>, &[a, b])?; - assert!((result + 22.0).abs() < f64::EPSILON); + let a = 10_i32.to_js(lock)?; + let b = 32_i32.to_js(lock)?; + let result = func.call::(lock, None::>, &[a, b])?; + assert_eq!(result, -22); Ok(()) }); } @@ -130,8 +130,8 @@ fn call_with_receiver() { .unwrap(); let func = v8::Local::::try_from(func_val).unwrap(); - let result = func.call::(lock, Some(obj), &[])?; - assert!((result - 100.0).abs() < f64::EPSILON); + let result = func.call::(lock, Some(obj), &[])?; + assert_eq!(result, 100); Ok(()) }); } @@ -187,9 +187,9 @@ fn try_catch_catches_thrown_non_error() { let func = ctx.eval_raw(lock, "(() => { throw 42; })").unwrap(); let func = v8::Local::::try_from(func).unwrap(); - let caught = lock.try_catch(|lock| func.call::(lock, None, &[]))?; - let thrown = f64::from_js(lock, caught.unwrap_err())?; - assert!((thrown - 42.0).abs() < f64::EPSILON); + let caught = lock.try_catch(|lock| func.call::(lock, None, &[]))?; + let thrown = u32::from_js(lock, caught.unwrap_err())?; + assert_eq!(thrown, 42); Ok(()) }); } @@ -217,11 +217,8 @@ fn try_catch_does_not_run_thrown_value_accessors() { let caught = lock.try_catch(|lock| func.call::>(lock, None, &[]))?; assert_eq!(caught.unwrap_err(), thrown); - let reads = ctx.eval::(lock, "reads").unwrap(); - assert!( - reads.abs() < f64::EPSILON, - "{thrown:?} ran {reads:?} accessors" - ); + let reads = ctx.eval::(lock, "reads").unwrap(); + assert_eq!(reads, 0, "{thrown:?} ran {reads} accessors"); } Ok(()) }); @@ -235,8 +232,8 @@ fn try_catch_returns_result_on_success() { let func = ctx.eval_raw(lock, "(() => 7)").unwrap(); let func = v8::Local::::try_from(func).unwrap(); - let result = lock.try_catch(|lock| func.call::(lock, None, &[]))?; - assert!((result.unwrap() - 7.0).abs() < f64::EPSILON); + let result = lock.try_catch(|lock| func.call::(lock, None, &[]))?; + assert_eq!(result.unwrap(), 7); Ok(()) }); } diff --git a/src/rust/jsg-test/tests/function_new.rs b/src/rust/jsg-test/tests/function_new.rs index 31c8cd9ce8d..12dbe666fb6 100644 --- a/src/rust/jsg-test/tests/function_new.rs +++ b/src/rust/jsg-test/tests/function_new.rs @@ -53,7 +53,7 @@ fn functions_keep_their_own_data() { .unwrap(); let count = v8::Function::new(lock, Cell::new(0u32), |lock, calls, _args| { calls.set(calls.get() + 1); - f64::from(calls.get()).to_js(lock) + calls.get().to_js(lock) }) .unwrap(); ctx.set_global(lock, "hello", v8::Local::::from(hello)); @@ -371,11 +371,11 @@ fn reentrant_call_sees_its_own_data() { // Each call logs its depth on entry and exit, and calls `again` in between, which calls // the same function again. No `RefCell` borrow is held across the call into JavaScript. let f = v8::Function::new(lock, RefCell::new(Vec::new()), |lock, log, args| { - let depth = f64::from_js(lock, args.get(0))?; + let depth = u32::from_js(lock, args.get(0))?; log.borrow_mut().push(format!("+{depth}")); - if depth > 0.0 { + if depth > 0 { let again: v8::Local = args.get(1).try_into().unwrap(); - let next = (depth - 1.0).to_js(lock)?; + let next = (depth - 1).to_js(lock)?; again.call::>(lock, None, &[next])?; } log.borrow_mut().push(format!("-{depth}")); diff --git a/src/rust/jsg-test/tests/gc.rs b/src/rust/jsg-test/tests/gc.rs index 6dc759e6381..0aee57c4b4c 100644 --- a/src/rust/jsg-test/tests/gc.rs +++ b/src/rust/jsg-test/tests/gc.rs @@ -904,7 +904,7 @@ fn nullable_ref_null_does_not_crash_during_gc() { static NATIVE_STATE_DROPS: AtomicUsize = AtomicUsize::new(0); struct NativeState { - value: u64, + value: u32, } impl jsg::Traced for NativeState {} @@ -923,10 +923,8 @@ struct NativeOwnerResource { #[jsg_resource] impl NativeOwnerResource { #[jsg_method] - fn get_value(&self) -> f64 { - #[expect(clippy::cast_precision_loss, reason = "test values are small")] - let value = self.state.value as f64; - value + fn get_value(&self) -> u32 { + self.state.value } } diff --git a/src/rust/jsg-test/tests/integer.rs b/src/rust/jsg-test/tests/integer.rs index 4d9601c4d8e..771c27480a2 100644 --- a/src/rust/jsg-test/tests/integer.rs +++ b/src/rust/jsg-test/tests/integer.rs @@ -4,12 +4,17 @@ //! Integer conversion matches C++ JSG (`jsg/value.h`): an integer converts to a //! number, and from JS, out-of-range values are a `TypeError`, and in-range -//! fractions truncate. +//! fractions truncate. `Integer`, `Int32` and `Uint32` handles are the counterparts of the V8 +//! classes, and `int32_value()` and the like convert as `v8::Value::Int32Value()` does. use jsg::ExceptionType; use jsg::FromJS; use jsg::NonCoercible; use jsg::ToJS; +use jsg::v8; +use jsg_macros::jsg_method; +use jsg_macros::jsg_resource; +use jsg_macros::jsg_struct; /// Evaluates `expr` and converts it to `T`, as an argument would be. fn convert<'s, T: FromJS<'s>>( @@ -141,6 +146,36 @@ fn integers_convert_to_numbers() { }); } +/// An integer returned in an `Option`, a `Nullable`, a `Vec` or a `#[jsg_struct]` field is a +/// number too. +#[test] +fn integers_in_containers_convert_to_numbers() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + install(lock, ctx); + assert!(ctx.eval::(lock, "r.first([7, 8]) === 7").unwrap()); + assert!( + ctx.eval::(lock, "r.first([-(2 ** 31)]) === -2147483648") + .unwrap() + ); + assert!(ctx.eval::(lock, "r.first([]) === undefined").unwrap()); + assert!( + ctx.eval::(lock, "r.max([1, 2 ** 32 - 1]) === 4294967295") + .unwrap() + ); + assert!(ctx.eval::(lock, "r.max([]) === null").unwrap()); + assert!( + ctx.eval::( + lock, + "const b = r.bounds(); + b.min === -2147483648 && b.max === 4294967295 && b.small === -1" + ) + .unwrap() + ); + Ok(()) + }); +} + /// A `Vec` or slice of integers converts from an `Array` whose elements convert, and not from a /// typed array, which is a `Local` and the like. #[test] @@ -152,6 +187,10 @@ fn vec_of_integers_converts_from_an_array() { vec![1, 2, 255] ); assert_eq!(convert::<&[u32]>(lock, ctx, "[7]").unwrap(), vec![7]); + assert_eq!( + convert::>(lock, ctx, "[-1, 2]").unwrap(), + vec![-1, 2] + ); assert_eq!( convert::>(lock, ctx, "[1n, -2, 3.9]").unwrap(), vec![1, -2, 3] @@ -239,3 +278,303 @@ fn non_coercible_integers_require_a_number() { Ok(()) }); } + +#[jsg_struct] +struct Bounds { + pub min: i32, + pub max: u32, + pub small: i8, +} + +#[jsg_resource] +struct Integers; + +#[jsg_resource] +impl Integers { + #[jsg_method] + pub fn int32(&self, value: v8::Local<'_, v8::Int32>) -> i32 { + value.value() + } + + #[jsg_method] + pub fn uint32(&self, value: v8::Local<'_, v8::Uint32>) -> u32 { + value.value() + } + + #[jsg_method] + pub fn integer(&self, value: v8::Local<'_, v8::Integer>) -> i64 { + value.value() + } + + #[jsg_method] + pub fn first(&self, values: Vec) -> Option { + values.first().copied() + } + + #[jsg_method] + pub fn max(&self, values: Vec) -> jsg::Nullable { + values.iter().max().copied().into() + } + + #[jsg_method] + pub fn bounds(&self) -> Bounds { + Bounds { + min: i32::MIN, + max: u32::MAX, + small: -1, + } + } + + #[jsg_method] + pub fn to_int32<'s>( + &self, + lock: &mut jsg::Lock<'s>, + value: v8::Local<'s, v8::Value>, + ) -> Result { + value.int32_value(lock) + } +} + +fn install(lock: &mut jsg::Lock<'_>, ctx: &crate::EvalContext<'_>) { + let resource = jsg::Rc::new(Integers).to_js(lock).unwrap(); + ctx.set_global(lock, "r", resource); +} + +/// `Integer::new` and `new_from_unsigned` make numbers, which cast to `Int32` or `Uint32` when +/// they are in range, and the checks for all three match V8's. +#[test] +fn integer_handles() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let signed = v8::Integer::new(lock, -7); + assert_eq!(signed.value(), -7); + let number: v8::Local = signed.into(); + assert_eq!(number.value().to_bits(), (-7.0_f64).to_bits()); + let value: v8::Local = signed.into(); + assert!(value.is_number()); + assert!(value.is_int32()); + assert!(!value.is_uint32()); + assert_eq!(v8::Local::::try_from(value).unwrap().value(), -7); + assert!(v8::Local::::try_from(value).is_err()); + let integer: v8::Local = number.try_into().unwrap(); + assert!(v8::Local::::try_from(integer).is_ok()); + assert!(v8::Local::::try_from(integer).is_err()); + ctx.set_global(lock, "v", signed); + assert!(ctx.eval::(lock, "v === -7").unwrap()); + + let unsigned: v8::Local = v8::Integer::new_from_unsigned(lock, u32::MAX).into(); + assert!(!unsigned.is_int32()); + assert!(unsigned.is_uint32()); + let uint32 = v8::Local::::try_from(unsigned).unwrap(); + assert_eq!(uint32.value(), u32::MAX); + assert_eq!( + v8::Local::::from(uint32).value(), + i64::from(u32::MAX) + ); + assert!(v8::Local::::try_from(unsigned).is_ok()); + assert!(v8::Local::::try_from(unsigned).is_err()); + let traced = v8::TracedGlobal::new(lock, uint32); + assert_eq!(traced.as_local(lock).value(), u32::MAX); + ctx.set_global(lock, "v", unsigned); + assert!(ctx.eval::(lock, "v === 4294967295").unwrap()); + + let zero: v8::Local = v8::Integer::new(lock, 0).into(); + assert!(zero.is_int32()); + assert!(zero.is_uint32()); + + // A number out of both ranges, a fraction, `-0`, or anything that is not a number, is + // none of the three. + for expr in [ + "2 ** 32", + "-(2 ** 31) - 1", + "1.5", + "-0", + "NaN", + "Infinity", + "'1'", + "1n", + "new Number(1)", + ] { + let not_integer = ctx.eval_raw(lock, expr).unwrap(); + assert!(!not_integer.is_int32(), "{expr}"); + assert!(!not_integer.is_uint32(), "{expr}"); + assert!( + v8::Local::::try_from(not_integer).is_err(), + "{expr}" + ); + } + Ok(()) + }); +} + +/// A handle parameter takes a number in range as it is, without `ToNumber` or truncation. +#[test] +fn integer_handle_parameters_do_not_coerce() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + install(lock, ctx); + assert_eq!( + ctx.eval::(lock, "r.int32(-(2 ** 31))").unwrap(), + i32::MIN + ); + assert_eq!( + ctx.eval::(lock, "r.uint32(2 ** 32 - 1)").unwrap(), + u32::MAX + ); + assert_eq!( + ctx.eval::(lock, "r.integer(2 ** 31)").unwrap(), + 1 << 31 + ); + assert_eq!(ctx.eval::(lock, "r.integer(-1)").unwrap(), -1); + + for (expr, expected) in [ + ("r.int32(1.5)", "Int32"), + ("r.int32(2 ** 31)", "Int32"), + ("r.int32('7')", "Int32"), + ("r.int32({ valueOf() { return 7; } })", "Int32"), + ("r.uint32(-1)", "Uint32"), + ("r.uint32(2 ** 32)", "Uint32"), + ("r.integer(2 ** 32)", "Integer"), + ("r.integer(-(2 ** 31) - 1)", "Integer"), + ("r.integer(0.5)", "Integer"), + ] { + let err = ctx + .eval::(lock, expr) + .unwrap_err() + .unwrap_jsg_err(lock); + assert_eq!(err.name, ExceptionType::TypeError, "{expr}"); + assert!(err.message.contains(expected), "{expr}: {}", err.message); + } + + // `Lenient` applies the same check through `try_from_js`. + let lenient: jsg::Lenient> = ctx.eval(lock, "7").unwrap(); + assert!(matches!(lenient, jsg::Lenient::Some(n) if n.value() == 7)); + let lenient: jsg::Lenient> = ctx.eval(lock, "-1").unwrap(); + assert!(matches!(lenient, jsg::Lenient::Unconvertable(_))); + Ok(()) + }); +} + +/// `int32_value`, `uint32_value` and `integer_value` convert any value as JavaScript's +/// `ToInt32`, `ToUint32` and `ToInteger` do, running `valueOf` where there is one. +#[test] +fn integer_coercions() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let value_of = "({ valueOf() { return -2.5; } })"; + for (expr, expected) in [ + ("7", 7), + ("-7", -7), + ("1.9", 1), + ("-1.9", -1), + ("-0", 0), + ("NaN", 0), + ("Infinity", 0), + ("-Infinity", 0), + ("2 ** 32 + 5", 5), + ("2 ** 31", i32::MIN), + ("'7'", 7), + ("' 0x10 '", 16), + ("true", 1), + ("null", 0), + ("undefined", 0), + ("[3]", 3), + (value_of, -2), + ] { + let value = ctx.eval_raw(lock, expr).unwrap(); + assert_eq!(value.int32_value(lock).unwrap(), expected, "{expr}"); + } + for (expr, expected) in [ + ("7", 7), + ("-1", u32::MAX), + ("2 ** 32 + 5", 5), + ("2 ** 31", 1 << 31), + ("NaN", 0), + ("-0", 0), + ("'7'", 7), + (value_of, u32::MAX - 1), + ] { + let value = ctx.eval_raw(lock, expr).unwrap(); + assert_eq!(value.uint32_value(lock).unwrap(), expected, "{expr}"); + } + for (expr, expected) in [ + ("1.9", 1), + ("-1.9", -1), + ("NaN", 0), + ("2 ** 32 + 5", (1 << 32) + 5), + ("2 ** 53", 1 << 53), + ("2 ** 63", i64::MAX), + ("-(2 ** 63)", i64::MIN), + ("Infinity", i64::MAX), + ("-Infinity", i64::MIN), + ("'7'", 7), + (value_of, -2), + ] { + let value = ctx.eval_raw(lock, expr).unwrap(); + assert_eq!(value.integer_value(lock).unwrap(), expected, "{expr}"); + } + Ok(()) + }); +} + +#[derive(Clone, Copy, Debug)] +enum Coercion { + Int32, + Uint32, + Integer, +} + +/// What `valueOf` throws, or `ToNumber` throws for a `BigInt` or a `Symbol`, stays pending, so +/// that a method passing the error on throws it to its caller. +#[test] +fn integer_coercions_pass_on_what_to_number_throws() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let thrower = "({ valueOf() { throw new RangeError('boom'); } })"; + for (expr, name, message) in [ + (thrower, ExceptionType::RangeError, "boom"), + ( + "1n", + ExceptionType::TypeError, + "Cannot convert a BigInt value to a number", + ), + ( + "Symbol()", + ExceptionType::TypeError, + "Cannot convert a Symbol value to a number", + ), + ] { + let value = ctx.eval_raw(lock, expr).unwrap(); + for coercion in [Coercion::Int32, Coercion::Uint32, Coercion::Integer] { + let caught = lock + .try_catch(|lock| { + let err = match coercion { + Coercion::Int32 => value.int32_value(lock).map(drop), + Coercion::Uint32 => value.uint32_value(lock).map(drop), + Coercion::Integer => value.integer_value(lock).map(drop), + } + .unwrap_err(); + assert!(err.is_pending(), "{expr}"); + Err::<(), _>(err) + }) + .expect("execution should not be terminating"); + let Err(exception) = caught else { + panic!("{expr} should not convert"); + }; + let err = jsg::Error::from_js(lock, exception).unwrap(); + assert_eq!(err.name, name, "{expr} ({coercion:?})"); + assert_eq!(err.message, message, "{expr} ({coercion:?})"); + } + } + + install(lock, ctx); + assert_eq!(ctx.eval::(lock, "r.toInt32('0x10')").unwrap(), 16); + let err = ctx + .eval::(lock, &format!("r.toInt32({thrower})")) + .unwrap_err() + .unwrap_jsg_err(lock); + assert_eq!(err.name, ExceptionType::RangeError); + assert_eq!(err.message, "boom"); + Ok(()) + }); +} diff --git a/src/rust/jsg-test/tests/isolates.rs b/src/rust/jsg-test/tests/isolates.rs index 1ca024fc32f..055770cc648 100644 --- a/src/rust/jsg-test/tests/isolates.rs +++ b/src/rust/jsg-test/tests/isolates.rs @@ -299,7 +299,7 @@ fn local_of_another_isolate_panics_in_debug_builds() { let b = crate::Harness::new(); a.run_in_context(|lock_a, _ctx| { let object: v8::Local = lock_a.new_object().into(); - let number = 1.0_f64.to_js(lock_a)?; + let number = 1_u32.to_js(lock_a)?; // SAFETY: deliberately breaks the rule to check that debug builds catch it; the // handle of A is rejected before it reaches V8. unsafe { @@ -311,7 +311,7 @@ fn local_of_another_isolate_panics_in_debug_builds() { // A Smi belongs to no isolate. let global = v8::TracedGlobal::new(lock_b, number); let local = global.as_local(lock_b); - assert!((f64::from_js(lock_b, local)? - 1.0).abs() < f64::EPSILON); + assert_eq!(u32::from_js(lock_b, local)?, 1); Ok(()) }); }); @@ -343,7 +343,7 @@ fn function_call_with_argument_of_another_isolate_panics_in_debug_builds() { .try_into() .unwrap(); assert_isolate_check_panics("calling B's function with A's object", || { - let _ = func.call::(lock_b, None, &[object]); + let _ = func.call::(lock_b, None, &[object]); }); Ok(()) }); diff --git a/src/rust/jsg-test/tests/local_cast.rs b/src/rust/jsg-test/tests/local_cast.rs index 29cc6bbacda..ddbc21527a9 100644 --- a/src/rust/jsg-test/tests/local_cast.rs +++ b/src/rust/jsg-test/tests/local_cast.rs @@ -182,8 +182,7 @@ fn array_into_object() { let obj: v8::Local = arr.into(); assert!(obj.has(lock, "length").unwrap()); let prop = obj.get(lock, "length").unwrap(); - let len: f64 = ::from_js(lock, prop)?; - assert!((len - 3.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, prop)?, 3); Ok(()) }); } @@ -201,8 +200,7 @@ fn typed_array_into_object() { let obj: v8::Local = ta.into(); assert!(obj.has(lock, "byteLength").unwrap()); let prop = obj.get(lock, "byteLength").unwrap(); - let byte_len: f64 = ::from_js(lock, prop)?; - assert!((byte_len - 3.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, prop)?, 3); Ok(()) }); } @@ -242,8 +240,7 @@ fn array_as_object_property_access() { assert!(obj.has(lock, "length").unwrap()); let prop = obj.get(lock, "length").unwrap(); - let val: f64 = ::from_js(lock, prop)?; - assert!((val - 3.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, prop)?, 3); Ok(()) }); } @@ -260,11 +257,9 @@ fn typed_array_as_object_property_access() { assert!(obj.has(lock, "byteLength").unwrap()); assert!(obj.has(lock, "length").unwrap()); let prop = obj.get(lock, "byteLength").unwrap(); - let byte_len: f64 = ::from_js(lock, prop)?; - assert!((byte_len - 5.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, prop)?, 5); let prop = obj.get(lock, "length").unwrap(); - let len: f64 = ::from_js(lock, prop)?; - assert!((len - 5.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, prop)?, 5); Ok(()) }); } @@ -363,8 +358,7 @@ fn function_object_roundtrip() { assert_eq!(name, "namedFn"); // Roundtrip back and verify it's still callable let back: v8::Local = obj.try_into().unwrap(); - let result = back.call::(lock, None, &[])?; - assert!((result - 42.0).abs() < f64::EPSILON); + assert_eq!(back.call::(lock, None, &[])?, 42); Ok(()) }); } @@ -381,8 +375,7 @@ fn array_object_roundtrip() { let obj: v8::Local = arr.into(); // Verify length via Object property access let prop = obj.get(lock, "length").unwrap(); - let len: f64 = ::from_js(lock, prop)?; - assert!((len - 3.0).abs() < f64::EPSILON); + assert_eq!(::from_js(lock, prop)?, 3); // Roundtrip back and verify Array-specific methods work let back: v8::Local = obj.try_into().unwrap(); assert_eq!(back.len(), 3); diff --git a/src/rust/jsg-test/tests/promise.rs b/src/rust/jsg-test/tests/promise.rs index 59390d6ceb3..2228d91eaab 100644 --- a/src/rust/jsg-test/tests/promise.rs +++ b/src/rust/jsg-test/tests/promise.rs @@ -44,7 +44,7 @@ fn resolver_settles_its_promise_once() { assert_eq!(promise.state(), PromiseState::Pending); assert!(promise.result(lock).is_none()); - let value = 42.0_f64.to_js(lock)?; + let value = 42_u32.to_js(lock)?; resolver.resolve(lock, value)?; let other = "ignored".to_js(lock)?; // Settling a settled promise does nothing. @@ -52,10 +52,7 @@ fn resolver_settles_its_promise_once() { assert_eq!(promise.state(), PromiseState::Fulfilled); let result = promise.result(lock).unwrap(); - #[expect(clippy::float_cmp, reason = "42 is exactly representable")] - { - assert_eq!(f64::from_js(lock, result)?, 42.0); - } + assert_eq!(u32::from_js(lock, result)?, 42); Ok(()) }); } diff --git a/src/rust/jsg-test/tests/resource_callback.rs b/src/rust/jsg-test/tests/resource_callback.rs index bddb3ac7ca1..b7fd5564cf8 100644 --- a/src/rust/jsg-test/tests/resource_callback.rs +++ b/src/rust/jsg-test/tests/resource_callback.rs @@ -58,8 +58,8 @@ impl DirectReturnResource { } #[jsg_method] - pub fn get_counter(&self) -> f64 { - f64::from(self.counter.get()) + pub fn get_counter(&self) -> u32 { + self.counter.get() } #[jsg_method] @@ -154,8 +154,7 @@ fn resource_method_returns_non_result_values() { assert!(result); // Test getCounter returns number - let result: f64 = ctx.eval(lock, "resource.getCounter()").unwrap(); - assert!((result - 42.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "resource.getCounter()").unwrap(), 42); // Test incrementCounter returns undefined (we just check it doesn't error) let _: Option = ctx.eval(lock, "resource.incrementCounter()").unwrap(); @@ -174,7 +173,7 @@ struct MathResource; #[jsg_resource] impl MathResource { #[jsg_method] - pub fn add(a: f64, b: f64) -> f64 { + pub fn add(a: i32, b: i32) -> i32 { a + b } @@ -205,8 +204,7 @@ fn static_method_callable_on_class() { let constructor = jsg::resource::function_template_of::(lock); ctx.set_global(lock, "MathResource", constructor); - let result: f64 = ctx.eval(lock, "MathResource.add(2, 3)").unwrap(); - assert!((result - 5.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "MathResource.add(2, 3)").unwrap(), 5); let result: String = ctx.eval(lock, "MathResource.greet('World')").unwrap(); assert_eq!(result, "Hello, World!"); @@ -233,8 +231,10 @@ fn instance_and_static_methods_coexist() { assert_eq!(result, "math"); // Static method works on the class - let result: f64 = ctx.eval(lock, "MathResource.add(10, 20)").unwrap(); - assert!((result - 30.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "MathResource.add(10, 20)").unwrap(), + 30 + ); // Static methods are NOT on the instance let is_undefined: bool = ctx.eval(lock, "typeof math.add === 'undefined'").unwrap(); @@ -333,14 +333,19 @@ fn static_constant_accessible_on_constructor() { let constructor = jsg::resource::function_template_of::(lock); ctx.set_global(lock, "ConstantResource", constructor); - let result: f64 = ctx.eval(lock, "ConstantResource.MAX_SIZE").unwrap(); - assert!((result - 1024.0).abs() < f64::EPSILON); - - let result: f64 = ctx.eval(lock, "ConstantResource.STATUS_OK").unwrap(); - assert!((result - 0.0).abs() < f64::EPSILON); - - let result: f64 = ctx.eval(lock, "ConstantResource.STATUS_ERROR").unwrap(); - assert!((result - (-1.0)).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "ConstantResource.MAX_SIZE").unwrap(), + 1024 + ); + assert_eq!( + ctx.eval::(lock, "ConstantResource.STATUS_OK").unwrap(), + 0 + ); + assert_eq!( + ctx.eval::(lock, "ConstantResource.STATUS_ERROR") + .unwrap(), + -1 + ); let result: f64 = ctx.eval(lock, "ConstantResource.SCALE_FACTOR").unwrap(); assert!((result - 2.5).abs() < f64::EPSILON); @@ -357,11 +362,8 @@ fn static_constant_accessible_on_instance() { let wrapped = resource.to_js(lock)?; ctx.set_global(lock, "obj", wrapped); - let result: f64 = ctx.eval(lock, "obj.MAX_SIZE").unwrap(); - assert!((result - 1024.0).abs() < f64::EPSILON); - - let result: f64 = ctx.eval(lock, "obj.STATUS_OK").unwrap(); - assert!((result - 0.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "obj.MAX_SIZE").unwrap(), 1024); + assert_eq!(ctx.eval::(lock, "obj.STATUS_OK").unwrap(), 0); Ok(()) }); } @@ -376,13 +378,13 @@ fn static_constant_is_read_only() { // Attempt to overwrite should silently fail (strict mode would throw). // The value should remain unchanged. - let result: f64 = ctx + let result: u32 = ctx .eval( lock, "ConstantResource.MAX_SIZE = 9999; ConstantResource.MAX_SIZE", ) .unwrap(); - assert!((result - 1024.0).abs() < f64::EPSILON); + assert_eq!(result, 1024); Ok(()) }); } @@ -405,12 +407,13 @@ fn static_constant_coexists_with_methods() { assert_eq!(result, "constant_resource"); // Static constant works on constructor - let result: f64 = ctx.eval(lock, "ConstantResource.MAX_SIZE").unwrap(); - assert!((result - 1024.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "ConstantResource.MAX_SIZE").unwrap(), + 1024 + ); // Static constant works on instance - let result: f64 = ctx.eval(lock, "obj.MAX_SIZE").unwrap(); - assert!((result - 1024.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "obj.MAX_SIZE").unwrap(), 1024); Ok(()) }); } @@ -994,8 +997,12 @@ fn resource_without_constructor_throws() { let constructor = jsg::resource::function_template_of::(lock); ctx.set_global(lock, "EchoResource", constructor); - let result: Result = ctx.eval(lock, "new EchoResource('hi')"); - assert!(result.is_err(), "should throw illegal constructor"); + let err = ctx + .eval::>(lock, "new EchoResource('hi')") + .unwrap_err() + .unwrap_jsg_err(lock); + assert_eq!(err.name, ExceptionType::TypeError); + assert_eq!(err.message, "Illegal constructor"); Ok(()) }); } @@ -1077,18 +1084,18 @@ fn constructor_instanceof_works() { #[jsg_resource] struct Counter { - value: f64, + value: u32, } #[jsg_resource] impl Counter { #[jsg_constructor] - fn constructor(_lock: &mut jsg::Lock, value: f64) -> Self { + fn constructor(_lock: &mut jsg::Lock, value: u32) -> Self { Self { value } } #[jsg_method] - fn get_value(&self) -> f64 { + fn get_value(&self) -> u32 { self.value } } @@ -1101,8 +1108,10 @@ fn constructor_with_lock_parameter() { let constructor = jsg::resource::function_template_of::(lock); ctx.set_global(lock, "Counter", constructor); - let result: f64 = ctx.eval(lock, "new Counter(99).getValue()").unwrap(); - assert!((result - 99.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "new Counter(99).getValue()").unwrap(), + 99 + ); Ok(()) }); } @@ -1147,14 +1156,14 @@ fn constructor_with_wrong_resource_type_throws_type_error() { #[jsg_resource] struct Positive { - value: f64, + value: i32, } #[jsg_resource] impl Positive { #[jsg_constructor] - fn constructor(value: f64) -> jsg::Result { - if value > 0.0 { + fn constructor(value: i32) -> jsg::Result { + if value > 0 { Ok(Self { value }) } else { Err(jsg::Error::new_range_error("value must be positive")) @@ -1162,7 +1171,7 @@ impl Positive { } #[jsg_method] - fn get_value(&self) -> f64 { + fn get_value(&self) -> i32 { self.value } } @@ -1232,14 +1241,14 @@ fn fallible_constructor_err_throws() { ctx.set_global(lock, "Label", label); let err = ctx - .eval::(lock, "new Positive(-1)") + .eval::>(lock, "new Positive(-1)") .unwrap_err() .unwrap_jsg_err(lock); assert_eq!(err.name, ExceptionType::RangeError); assert_eq!(err.message, "value must be positive"); let err = ctx - .eval::(lock, "new Label('')") + .eval::>(lock, "new Label('')") .unwrap_err() .unwrap_jsg_err(lock); assert_eq!(err.name, ExceptionType::TypeError); diff --git a/src/rust/jsg-test/tests/resource_conversion.rs b/src/rust/jsg-test/tests/resource_conversion.rs index fefde7c6565..73329a5634b 100644 --- a/src/rust/jsg-test/tests/resource_conversion.rs +++ b/src/rust/jsg-test/tests/resource_conversion.rs @@ -31,13 +31,13 @@ impl Greeter { #[jsg_resource] struct Counter { - value: f64, + value: i32, } #[jsg_resource] impl Counter { #[jsg_method] - pub fn get_value(&self) -> f64 { + pub fn get_value(&self) -> i32 { self.value } } @@ -68,12 +68,12 @@ fn to_js_creates_object_with_methods() { fn to_js_works_for_different_resource_types() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let counter = Counter { value: 42.0 }; + let counter = Counter { value: 42 }; let js_val = counter.to_js(lock)?; ctx.set_global(lock, "counter", js_val); - let result: f64 = ctx.eval(lock, "counter.getValue()").unwrap(); - assert!((result - 42.0).abs() < f64::EPSILON); + let result: i32 = ctx.eval(lock, "counter.getValue()").unwrap(); + assert_eq!(result, 42); Ok(()) }); } diff --git a/src/rust/jsg-test/tests/resource_lazy_properties.rs b/src/rust/jsg-test/tests/resource_lazy_properties.rs index 2c662f76fdc..a95955afc63 100644 --- a/src/rust/jsg-test/tests/resource_lazy_properties.rs +++ b/src/rust/jsg-test/tests/resource_lazy_properties.rs @@ -267,18 +267,18 @@ fn custom_members_select_lazy_instance_placement() { #[jsg_resource] struct Settable { - value: Cell, + value: Cell, } #[jsg_resource(custom_members)] impl Settable { #[jsg_property] - pub fn get_value(&self) -> f64 { + pub fn get_value(&self) -> i32 { self.value.get() } #[jsg_property] - pub fn set_value(&self, value: f64) { + pub fn set_value(&self, value: i32) { self.value.set(value); } } diff --git a/src/rust/jsg-test/tests/resource_properties.rs b/src/rust/jsg-test/tests/resource_properties.rs index 043d78a84f9..2e2848ffeea 100644 --- a/src/rust/jsg-test/tests/resource_properties.rs +++ b/src/rust/jsg-test/tests/resource_properties.rs @@ -21,7 +21,7 @@ use jsg_macros::jsg_static_constant; /// A counter with a read/write value, a read-only label, and an inspect property. #[jsg_resource] struct Counter { - value: Cell, + value: Cell, label: RefCell, } @@ -29,12 +29,12 @@ struct Counter { impl Counter { // Read/write prototype property — detected from get_/set_ prefix. #[jsg_property(prototype)] - pub fn get_value(&self) -> f64 { + pub fn get_value(&self) -> i32 { self.value.get() } #[jsg_property(prototype)] - pub fn set_value(&self, v: f64) { + pub fn set_value(&self, v: i32) { self.value.set(v); } @@ -47,7 +47,7 @@ impl Counter { // A regular method coexisting with properties. #[jsg_method] pub fn reset(&self) { - self.value.set(0.0); + self.value.set(0); } // Inspect property — hidden from normal enumeration. @@ -62,11 +62,11 @@ impl Counter { // Static constant coexisting with properties. #[jsg_static_constant] - pub const MAX_VALUE: f64 = 1_000_000.0; + pub const MAX_VALUE: u32 = 1_000_000; } impl Counter { - fn new(value: f64, label: impl Into) -> Self { + fn new(value: i32, label: impl Into) -> Self { Self { value: Cell::new(value), label: RefCell::new(label.into()), @@ -146,29 +146,29 @@ impl MultiWord { /// A resource with an explicit name override. #[jsg_resource] struct ExplicitName { - x: Cell, + x: Cell, } #[jsg_resource] impl ExplicitName { #[jsg_property(prototype, name = "myValue")] - pub fn get_something(&self) -> f64 { + pub fn get_something(&self) -> i32 { self.x.get() } #[jsg_property(prototype, name = "myValue")] - pub fn set_something(&self, v: f64) { + pub fn set_something(&self, v: i32) { self.x.set(v); } #[jsg_inspect_property(name = "debugX")] - pub fn internal_debug(&self) -> f64 { + pub fn internal_debug(&self) -> i32 { self.x.get() } } impl ExplicitName { - fn new(x: f64) -> Self { + fn new(x: i32) -> Self { Self { x: Cell::new(x) } } } @@ -214,10 +214,9 @@ impl MaybeHolder { fn prototype_getter_returns_initial_value() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(42.0, "x")); + let r = jsg::Rc::new(Counter::new(42, "x")); ctx.set_global(lock, "c", r); - let v: f64 = ctx.eval(lock, "c.value").unwrap(); - assert!((v - 42.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "c.value").unwrap(), 42); Ok(()) }); } @@ -226,12 +225,11 @@ fn prototype_getter_returns_initial_value() { fn prototype_getter_reflects_rust_side_mutation() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); let r2 = r.clone(); ctx.set_global(lock, "c", r); - r2.value.set(99.0); - let v: f64 = ctx.eval(lock, "c.value").unwrap(); - assert!((v - 99.0).abs() < f64::EPSILON); + r2.value.set(99); + assert_eq!(ctx.eval::(lock, "c.value").unwrap(), 99); Ok(()) }); } @@ -240,7 +238,7 @@ fn prototype_getter_reflects_rust_side_mutation() { fn prototype_getter_returns_string_value() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(0.0, "hello")); + let r = jsg::Rc::new(Counter::new(0, "hello")); ctx.set_global(lock, "c", r); let v: String = ctx.eval(lock, "c.label").unwrap(); assert_eq!(v, "hello"); @@ -256,11 +254,10 @@ fn prototype_getter_returns_string_value() { fn prototype_setter_updates_value_visible_via_getter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(0.0, "x")); + let r = jsg::Rc::new(Counter::new(0, "x")); ctx.set_global(lock, "c", r); ctx.eval_raw(lock, "c.value = 77").unwrap(); - let v: f64 = ctx.eval(lock, "c.value").unwrap(); - assert!((v - 77.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "c.value").unwrap(), 77); Ok(()) }); } @@ -269,11 +266,11 @@ fn prototype_setter_updates_value_visible_via_getter() { fn prototype_setter_mutation_visible_from_rust() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(0.0, "x")); + let r = jsg::Rc::new(Counter::new(0, "x")); let r2 = r.clone(); ctx.set_global(lock, "c", r); ctx.eval_raw(lock, "c.value = 55").unwrap(); - assert!((r2.value.get() - 55.0).abs() < f64::EPSILON); + assert_eq!(r2.value.get(), 55); Ok(()) }); } @@ -282,12 +279,11 @@ fn prototype_setter_mutation_visible_from_rust() { fn prototype_setter_called_multiple_times() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(0.0, "x")); + let r = jsg::Rc::new(Counter::new(0, "x")); ctx.set_global(lock, "c", r); ctx.eval_raw(lock, "c.value = 1; c.value = 2; c.value = 3") .unwrap(); - let v: f64 = ctx.eval(lock, "c.value").unwrap(); - assert!((v - 3.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "c.value").unwrap(), 3); Ok(()) }); } @@ -300,7 +296,7 @@ fn prototype_setter_called_multiple_times() { fn prototype_readonly_property_throws_in_strict_mode() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "lbl")); + let r = jsg::Rc::new(Counter::new(1, "lbl")); ctx.set_global(lock, "c", r); let result = ctx.eval_raw(lock, "'use strict'; c.label = 'changed'"); assert!( @@ -315,7 +311,7 @@ fn prototype_readonly_property_throws_in_strict_mode() { fn prototype_readonly_property_silently_ignored_in_sloppy_mode() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "original")); + let r = jsg::Rc::new(Counter::new(1, "original")); ctx.set_global(lock, "c", r); // In sloppy mode, the assignment is silently ignored; value unchanged. ctx.eval_raw(lock, "c.label = 'changed'").unwrap(); @@ -333,7 +329,7 @@ fn prototype_readonly_property_silently_ignored_in_sloppy_mode() { fn prototype_property_not_in_object_keys() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); let keys: String = ctx.eval(lock, "Object.keys(c).join(',')").unwrap(); assert_eq!( @@ -348,7 +344,7 @@ fn prototype_property_not_in_object_keys() { fn prototype_property_found_by_in_operator() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); let found: bool = ctx.eval(lock, "'value' in c").unwrap(); assert!(found, "'in' must find prototype properties"); @@ -360,7 +356,7 @@ fn prototype_property_found_by_in_operator() { fn prototype_property_not_an_own_property() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); let own: bool = ctx .eval(lock, "Object.prototype.hasOwnProperty.call(c, 'value')") @@ -381,7 +377,7 @@ fn prototype_property_is_enumerable() { // layer matches this behaviour. let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); // value is a read/write prototype property. let enumerable: bool = ctx @@ -404,7 +400,7 @@ fn prototype_readonly_property_is_enumerable() { // (without DontEnum) for normal properties, making them enumerable. let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); // label is a read-only prototype property. let enumerable: bool = ctx @@ -426,7 +422,7 @@ fn prototype_property_appears_in_for_in() { // Enumerable prototype properties must appear in for...in loops. let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); let found: bool = ctx .eval( @@ -447,20 +443,17 @@ fn prototype_property_appears_in_for_in() { fn prototype_property_independent_across_instances() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let a = jsg::Rc::new(Counter::new(10.0, "a")); - let b = jsg::Rc::new(Counter::new(20.0, "b")); + let a = jsg::Rc::new(Counter::new(10, "a")); + let b = jsg::Rc::new(Counter::new(20, "b")); ctx.set_global(lock, "a", a); ctx.set_global(lock, "b", b); - let va: f64 = ctx.eval(lock, "a.value").unwrap(); - let vb: f64 = ctx.eval(lock, "b.value").unwrap(); - assert!((va - 10.0).abs() < f64::EPSILON); - assert!((vb - 20.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "a.value").unwrap(), 10); + assert_eq!(ctx.eval::(lock, "b.value").unwrap(), 20); ctx.eval_raw(lock, "a.value = 99").unwrap(); - let va2: f64 = ctx.eval(lock, "a.value").unwrap(); - let vb2: f64 = ctx.eval(lock, "b.value").unwrap(); - assert!((va2 - 99.0).abs() < f64::EPSILON); - assert!( - (vb2 - 20.0).abs() < f64::EPSILON, + assert_eq!(ctx.eval::(lock, "a.value").unwrap(), 99); + assert_eq!( + ctx.eval::(lock, "b.value").unwrap(), + 20, "setting a.value must not affect b" ); Ok(()) @@ -475,12 +468,15 @@ fn prototype_property_independent_across_instances() { fn prototype_property_coexists_with_method() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(5.0, "x")); + let r = jsg::Rc::new(Counter::new(5, "x")); ctx.set_global(lock, "c", r); ctx.eval_raw(lock, "c.value = 7").unwrap(); ctx.eval_raw(lock, "c.reset()").unwrap(); - let v: f64 = ctx.eval(lock, "c.value").unwrap(); - assert!((v).abs() < f64::EPSILON, "reset() must set value back to 0"); + assert_eq!( + ctx.eval::(lock, "c.value").unwrap(), + 0, + "reset() must set value back to 0" + ); Ok(()) }); } @@ -491,14 +487,15 @@ fn prototype_property_coexists_with_static_constant() { harness.run_in_context(|lock, ctx| { let constructor = jsg::resource::function_template_of::(lock); ctx.set_global(lock, "Counter", constructor); - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); - let v: f64 = ctx.eval(lock, "c.value").unwrap(); - assert!((v - 1.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "c.value").unwrap(), 1); - let max: f64 = ctx.eval(lock, "Counter.MAX_VALUE").unwrap(); - assert!((max - 1_000_000.0).abs() < f64::EPSILON); + assert_eq!( + ctx.eval::(lock, "Counter.MAX_VALUE").unwrap(), + 1_000_000 + ); Ok(()) }); } @@ -561,10 +558,9 @@ fn prototype_property_original_rust_name_not_exposed() { fn explicit_name_override_getter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(ExplicitName::new(42.0)); + let r = jsg::Rc::new(ExplicitName::new(42)); ctx.set_global(lock, "obj", r); - let v: f64 = ctx.eval(lock, "obj.myValue").unwrap(); - assert!((v - 42.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "obj.myValue").unwrap(), 42); Ok(()) }); } @@ -573,11 +569,10 @@ fn explicit_name_override_getter() { fn explicit_name_override_setter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(ExplicitName::new(0.0)); + let r = jsg::Rc::new(ExplicitName::new(0)); ctx.set_global(lock, "obj", r); ctx.eval_raw(lock, "obj.myValue = 7").unwrap(); - let v: f64 = ctx.eval(lock, "obj.myValue").unwrap(); - assert!((v - 7.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "obj.myValue").unwrap(), 7); Ok(()) }); } @@ -586,7 +581,7 @@ fn explicit_name_override_setter() { fn explicit_name_original_rust_name_not_exposed() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(ExplicitName::new(0.0)); + let r = jsg::Rc::new(ExplicitName::new(0)); ctx.set_global(lock, "obj", r); let undef: bool = ctx .eval(lock, "typeof obj.getSomething === 'undefined'") @@ -812,7 +807,7 @@ fn instance_property_independent_across_instances() { fn inspect_property_not_accessible_by_string_key() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); let undef: bool = ctx .eval(lock, "typeof c.debugInfo === 'undefined'") @@ -829,7 +824,7 @@ fn inspect_property_not_accessible_by_string_key() { fn inspect_property_not_in_object_keys() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); let keys: String = ctx.eval(lock, "Object.keys(c).join(',')").unwrap(); assert!( @@ -844,7 +839,7 @@ fn inspect_property_not_in_object_keys() { fn inspect_property_not_in_own_string_names() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); let names: String = ctx .eval(lock, "Object.getOwnPropertyNames(c).join(',')") @@ -861,7 +856,7 @@ fn inspect_property_not_in_own_string_names() { fn inspect_property_registered_under_a_symbol() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); // The property is registered under a v8::Symbol; at least one symbol must exist on // the prototype chain that has an accessor descriptor. @@ -893,7 +888,7 @@ fn inspect_property_registered_under_a_symbol() { fn inspect_property_getter_returns_correct_value() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(7.0, "test")); + let r = jsg::Rc::new(Counter::new(7, "test")); ctx.set_global(lock, "c", r); // Retrieve the symbol-keyed getter from the prototype and call it. let result: String = ctx @@ -930,7 +925,7 @@ fn inspect_property_getter_returns_correct_value() { fn inspect_explicit_name_override() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(ExplicitName::new(5.0)); + let r = jsg::Rc::new(ExplicitName::new(5)); ctx.set_global(lock, "obj", r); // The inspect property must not be accessible as a string key "debugX". let undef: bool = ctx.eval(lock, "typeof obj.debugX === 'undefined'").unwrap(); @@ -977,7 +972,7 @@ const FIND_INSPECT_DICT: &str = r" fn inspect_properties_dict_exists_on_prototype() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); let find_inspect_dict = ctx.eval_raw(lock, FIND_INSPECT_DICT).unwrap(); ctx.set_global(lock, "findInspectDict", find_inspect_dict); @@ -994,7 +989,7 @@ fn inspect_properties_dict_exists_on_prototype() { fn inspect_properties_dict_contains_property_name() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); let find_inspect_dict = ctx.eval_raw(lock, FIND_INSPECT_DICT).unwrap(); ctx.set_global(lock, "findInspectDict", find_inspect_dict); @@ -1011,7 +1006,7 @@ fn inspect_properties_dict_contains_property_name() { fn inspect_properties_dict_value_is_the_getter_symbol() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(3.0, "dbg")); + let r = jsg::Rc::new(Counter::new(3, "dbg")); ctx.set_global(lock, "c", r); let find_inspect_dict = ctx.eval_raw(lock, FIND_INSPECT_DICT).unwrap(); ctx.set_global(lock, "findInspectDict", find_inspect_dict); @@ -1044,7 +1039,7 @@ fn inspect_properties_dict_value_is_the_getter_symbol() { fn inspect_properties_dict_getter_callable_via_symbol() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(7.0, "test")); + let r = jsg::Rc::new(Counter::new(7, "test")); ctx.set_global(lock, "c", r); let find_inspect_dict = ctx.eval_raw(lock, FIND_INSPECT_DICT).unwrap(); ctx.set_global(lock, "findInspectDict", find_inspect_dict); @@ -1106,7 +1101,7 @@ fn inspect_properties_dict_explicit_name_override() { // the overridden JS name, not the Rust method name. let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(ExplicitName::new(5.0)); + let r = jsg::Rc::new(ExplicitName::new(5)); ctx.set_global(lock, "obj", r); let find_inspect_dict = ctx.eval_raw(lock, FIND_INSPECT_DICT).unwrap(); ctx.set_global(lock, "findInspectDict", find_inspect_dict); @@ -1135,7 +1130,7 @@ fn inspect_properties_dict_explicit_name_override() { fn prototype_property_getter_throws_on_wrong_receiver() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); // Extract the getter from the descriptor and call it with a plain object receiver. let result = ctx.eval_raw( @@ -1156,7 +1151,7 @@ fn prototype_property_getter_throws_on_wrong_receiver() { fn prototype_property_setter_throws_on_wrong_receiver() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Counter::new(1.0, "x")); + let r = jsg::Rc::new(Counter::new(1, "x")); ctx.set_global(lock, "c", r); let result = ctx.eval_raw( lock, @@ -1179,19 +1174,19 @@ fn prototype_property_setter_throws_on_wrong_receiver() { /// A resource that mixes both kinds of properties. #[jsg_resource] struct Mixed { - proto_val: Cell, + proto_val: Cell, instance_val: RefCell, } #[jsg_resource] impl Mixed { #[jsg_property(prototype)] - pub fn get_proto_val(&self) -> f64 { + pub fn get_proto_val(&self) -> i32 { self.proto_val.get() } #[jsg_property(prototype)] - pub fn set_proto_val(&self, v: f64) { + pub fn set_proto_val(&self, v: i32) { self.proto_val.set(v); } @@ -1207,7 +1202,7 @@ impl Mixed { } impl Mixed { - fn new(p: f64, i: impl Into) -> Self { + fn new(p: i32, i: impl Into) -> Self { Self { proto_val: Cell::new(p), instance_val: RefCell::new(i.into()), @@ -1219,7 +1214,7 @@ impl Mixed { fn mixed_prototype_not_own_instance_is_own() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Mixed::new(1.0, "hello")); + let r = jsg::Rc::new(Mixed::new(1, "hello")); ctx.set_global(lock, "m", r); let proto_own: bool = ctx @@ -1242,11 +1237,11 @@ fn mixed_prototype_not_own_instance_is_own() { fn mixed_both_properties_readable() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Mixed::new(3.0, "world")); + let r = jsg::Rc::new(Mixed::new(3, "world")); ctx.set_global(lock, "m", r); - let pv: f64 = ctx.eval(lock, "m.protoVal").unwrap(); + let pv: i32 = ctx.eval(lock, "m.protoVal").unwrap(); let iv: String = ctx.eval(lock, "m.instanceVal").unwrap(); - assert!((pv - 3.0).abs() < f64::EPSILON); + assert_eq!(pv, 3); assert_eq!(iv, "world"); Ok(()) }); @@ -1256,13 +1251,13 @@ fn mixed_both_properties_readable() { fn mixed_both_properties_writable() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(Mixed::new(0.0, "old")); + let r = jsg::Rc::new(Mixed::new(0, "old")); ctx.set_global(lock, "m", r); ctx.eval_raw(lock, "m.protoVal = 9; m.instanceVal = 'new'") .unwrap(); - let pv: f64 = ctx.eval(lock, "m.protoVal").unwrap(); + let pv: i32 = ctx.eval(lock, "m.protoVal").unwrap(); let iv: String = ctx.eval(lock, "m.instanceVal").unwrap(); - assert!((pv - 9.0).abs() < f64::EPSILON); + assert_eq!(pv, 9); assert_eq!(iv, "new"); Ok(()) }); @@ -1285,50 +1280,50 @@ fn mixed_both_properties_writable() { #[jsg_resource] struct AllCombinations { // backing values for each property - a: Cell, // prototype, no-name, rw - b: Cell, // prototype, no-name, readonly - c: Cell, // prototype, name, rw - d: Cell, // prototype, name, readonly + a: Cell, // prototype, no-name, rw + b: Cell, // prototype, no-name, readonly + c: Cell, // prototype, name, rw + d: Cell, // prototype, name, readonly e: RefCell, // instance, no-name, rw f: RefCell, // instance, no-name, readonly g: RefCell, // instance, name, rw h: RefCell, // instance, name, readonly // extra: name before readonly (attribute order independence) - i: Cell, // prototype, name + readonly (name first) - j: Cell, // instance, readonly + name (readonly first) + i: Cell, // prototype, name + readonly (name first) + j: Cell, // instance, readonly + name (readonly first) } #[jsg_resource] impl AllCombinations { // ---- prototype, no name, rw ------------------------------------------------ #[jsg_property(prototype)] - pub fn get_a(&self) -> f64 { + pub fn get_a(&self) -> i32 { self.a.get() } #[jsg_property(prototype)] - pub fn set_a(&self, v: f64) { + pub fn set_a(&self, v: i32) { self.a.set(v); } // ---- prototype, no name, readonly ------------------------------------------ #[jsg_property(prototype, readonly)] - pub fn get_b(&self) -> f64 { + pub fn get_b(&self) -> i32 { self.b.get() } // ---- prototype, with name, rw ---------------------------------------------- #[jsg_property(prototype, name = "namedC")] - pub fn get_c(&self) -> f64 { + pub fn get_c(&self) -> i32 { self.c.get() } #[jsg_property(prototype, name = "namedC")] - pub fn set_c(&self, v: f64) { + pub fn set_c(&self, v: i32) { self.c.set(v); } // ---- prototype, with name, readonly ---------------------------------------- #[jsg_property(prototype, name = "namedD", readonly)] - pub fn get_d(&self) -> f64 { + pub fn get_d(&self) -> i32 { self.d.get() } @@ -1366,13 +1361,13 @@ impl AllCombinations { // ---- attribute order: name before readonly (prototype) --------------------- #[jsg_property(prototype, name = "namedI", readonly)] - pub fn get_i(&self) -> f64 { + pub fn get_i(&self) -> i32 { self.i.get() } // ---- attribute order: readonly before name (instance) ---------------------- #[jsg_property(instance, readonly, name = "namedJ")] - pub fn get_j(&self) -> f64 { + pub fn get_j(&self) -> i32 { self.j.get() } } @@ -1380,16 +1375,16 @@ impl AllCombinations { impl AllCombinations { fn new() -> Self { Self { - a: Cell::new(1.0), - b: Cell::new(2.0), - c: Cell::new(3.0), - d: Cell::new(4.0), + a: Cell::new(1), + b: Cell::new(2), + c: Cell::new(3), + d: Cell::new(4), e: RefCell::new("e".into()), f: RefCell::new("f".into()), g: RefCell::new("g".into()), h: RefCell::new("h".into()), - i: Cell::new(9.0), - j: Cell::new(10.0), + i: Cell::new(9), + j: Cell::new(10), } } } @@ -1402,8 +1397,7 @@ fn combo_prototype_noname_rw_get() { harness.run_in_context(|lock, ctx| { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); - let v: f64 = ctx.eval(lock, "o.a").unwrap(); - assert!((v - 1.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.a").unwrap(), 1); Ok(()) }); } @@ -1415,8 +1409,7 @@ fn combo_prototype_noname_rw_set() { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); ctx.eval_raw(lock, "o.a = 99").unwrap(); - let v: f64 = ctx.eval(lock, "o.a").unwrap(); - assert!((v - 99.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.a").unwrap(), 99); Ok(()) }); } @@ -1443,8 +1436,7 @@ fn combo_prototype_noname_readonly_get() { harness.run_in_context(|lock, ctx| { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); - let v: f64 = ctx.eval(lock, "o.b").unwrap(); - assert!((v - 2.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.b").unwrap(), 2); Ok(()) }); } @@ -1491,8 +1483,7 @@ fn combo_prototype_named_rw_get() { harness.run_in_context(|lock, ctx| { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); - let v: f64 = ctx.eval(lock, "o.namedC").unwrap(); - assert!((v - 3.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.namedC").unwrap(), 3); Ok(()) }); } @@ -1504,8 +1495,7 @@ fn combo_prototype_named_rw_set() { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); ctx.eval_raw(lock, "o.namedC = 33").unwrap(); - let v: f64 = ctx.eval(lock, "o.namedC").unwrap(); - assert!((v - 33.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.namedC").unwrap(), 33); Ok(()) }); } @@ -1539,8 +1529,7 @@ fn combo_prototype_named_readonly_get() { harness.run_in_context(|lock, ctx| { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); - let v: f64 = ctx.eval(lock, "o.namedD").unwrap(); - assert!((v - 4.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.namedD").unwrap(), 4); Ok(()) }); } @@ -1781,8 +1770,7 @@ fn combo_attr_order_name_then_readonly_prototype_get() { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); // #[jsg_property(prototype, name = "namedI", readonly)] - let v: f64 = ctx.eval(lock, "o.namedI").unwrap(); - assert!((v - 9.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.namedI").unwrap(), 9); Ok(()) }); } @@ -1811,8 +1799,7 @@ fn combo_attr_order_readonly_then_name_instance_get() { let r = jsg::Rc::new(AllCombinations::new()); ctx.set_global(lock, "o", r); // #[jsg_property(instance, readonly, name = "namedJ")] - let v: f64 = ctx.eval(lock, "o.namedJ").unwrap(); - assert!((v - 10.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "o.namedJ").unwrap(), 10); Ok(()) }); } @@ -1905,33 +1892,33 @@ fn combo_instance_props_independent_across_instances() { /// property (`get_readonly_val`) to exercise prefix-based detection directly. #[jsg_resource] struct PrefixDetection { - count: Cell, - readonly_val: Cell, + count: Cell, + readonly_val: Cell, } #[jsg_resource] impl PrefixDetection { /// `get_count` → getter for JS property "count" #[jsg_property(prototype)] - pub fn get_count(&self) -> f64 { + pub fn get_count(&self) -> u32 { self.count.get() } /// `set_count` → setter for JS property "count" (same stem as `get_count`) #[jsg_property(prototype)] - pub fn set_count(&self, v: f64) { + pub fn set_count(&self, v: u32) { self.count.set(v); } /// `get_readonly_val` → getter-only for JS property "readonlyVal" #[jsg_property(prototype, readonly)] - pub fn get_readonly_val(&self) -> f64 { + pub fn get_readonly_val(&self) -> i32 { self.readonly_val.get() } } impl PrefixDetection { - fn new(count: f64, readonly_val: f64) -> Self { + fn new(count: u32, readonly_val: i32) -> Self { Self { count: Cell::new(count), readonly_val: Cell::new(readonly_val), @@ -1945,11 +1932,11 @@ impl PrefixDetection { fn get_prefix_identifies_getter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(42.0, 0.0)); + let r = jsg::Rc::new(PrefixDetection::new(42, 0)); ctx.set_global(lock, "obj", r); - let v: f64 = ctx.eval(lock, "obj.count").unwrap(); - assert!( - (v - 42.0).abs() < f64::EPSILON, + assert_eq!( + ctx.eval::(lock, "obj.count").unwrap(), + 42, "get_count must be accessible as JS property 'count'" ); Ok(()) @@ -1962,12 +1949,12 @@ fn get_prefix_identifies_getter() { fn set_prefix_identifies_setter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(0.0, 0.0)); + let r = jsg::Rc::new(PrefixDetection::new(0, 0)); ctx.set_global(lock, "obj", r); ctx.eval_raw(lock, "obj.count = 99").unwrap(); - let v: f64 = ctx.eval(lock, "obj.count").unwrap(); - assert!( - (v - 99.0).abs() < f64::EPSILON, + assert_eq!( + ctx.eval::(lock, "obj.count").unwrap(), + 99, "set_count must write to JS property 'count'" ); Ok(()) @@ -1981,7 +1968,7 @@ fn set_prefix_identifies_setter() { fn getter_setter_paired_by_matching_stem() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(5.0, 0.0)); + let r = jsg::Rc::new(PrefixDetection::new(5, 0)); ctx.set_global(lock, "obj", r); // Both getter and setter must share the single JS name. let descriptor: bool = ctx @@ -2006,7 +1993,7 @@ fn getter_setter_paired_by_matching_stem() { fn getter_raw_rust_name_not_exposed() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(1.0, 0.0)); + let r = jsg::Rc::new(PrefixDetection::new(1, 0)); ctx.set_global(lock, "obj", r); let undef: bool = ctx .eval(lock, "typeof obj.getCount === 'undefined'") @@ -2024,7 +2011,7 @@ fn getter_raw_rust_name_not_exposed() { fn setter_raw_rust_name_not_exposed() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(1.0, 0.0)); + let r = jsg::Rc::new(PrefixDetection::new(1, 0)); ctx.set_global(lock, "obj", r); let undef: bool = ctx .eval(lock, "typeof obj.setCount === 'undefined'") @@ -2044,11 +2031,11 @@ fn setter_raw_rust_name_not_exposed() { fn getter_only_method_produces_readonly_property() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(0.0, 7.0)); + let r = jsg::Rc::new(PrefixDetection::new(0, 7)); ctx.set_global(lock, "obj", r); - let v: f64 = ctx.eval(lock, "obj.readonlyVal").unwrap(); - assert!( - (v - 7.0).abs() < f64::EPSILON, + assert_eq!( + ctx.eval::(lock, "obj.readonlyVal").unwrap(), + 7, "get_readonly_val must be readable as 'readonlyVal'" ); // No setter in the descriptor. @@ -2071,7 +2058,7 @@ fn getter_only_method_produces_readonly_property() { fn getter_only_throws_on_write_in_strict_mode() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(PrefixDetection::new(0.0, 3.0)); + let r = jsg::Rc::new(PrefixDetection::new(0, 3)); ctx.set_global(lock, "obj", r); let result = ctx.eval_raw(lock, "'use strict'; obj.readonlyVal = 99"); assert!( @@ -2094,7 +2081,7 @@ fn getter_only_throws_on_write_in_strict_mode() { #[jsg_resource] struct DefaultPlacement { name: RefCell, - version: Cell, + version: Cell, } #[jsg_resource] @@ -2112,13 +2099,13 @@ impl DefaultPlacement { /// Getter-only with default placement. #[jsg_property(readonly)] - pub fn get_version(&self) -> f64 { + pub fn get_version(&self) -> u32 { self.version.get() } } impl DefaultPlacement { - fn new(name: impl Into, version: f64) -> Self { + fn new(name: impl Into, version: u32) -> Self { Self { name: RefCell::new(name.into()), version: Cell::new(version), @@ -2132,7 +2119,7 @@ impl DefaultPlacement { fn default_placement_is_prototype_not_instance() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(DefaultPlacement::new("hello", 1.0)); + let r = jsg::Rc::new(DefaultPlacement::new("hello", 1)); ctx.set_global(lock, "obj", r); // Prototype property → not an own property. let own: bool = ctx @@ -2157,7 +2144,7 @@ fn default_placement_is_prototype_not_instance() { fn default_placement_getter_returns_value() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(DefaultPlacement::new("world", 2.0)); + let r = jsg::Rc::new(DefaultPlacement::new("world", 2)); ctx.set_global(lock, "obj", r); let v: String = ctx.eval(lock, "obj.name").unwrap(); assert_eq!(v, "world"); @@ -2170,7 +2157,7 @@ fn default_placement_getter_returns_value() { fn default_placement_setter_updates_value() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(DefaultPlacement::new("before", 0.0)); + let r = jsg::Rc::new(DefaultPlacement::new("before", 0)); ctx.set_global(lock, "obj", r); ctx.eval_raw(lock, "obj.name = 'after'").unwrap(); let v: String = ctx.eval(lock, "obj.name").unwrap(); @@ -2185,10 +2172,9 @@ fn default_placement_setter_updates_value() { fn default_placement_readonly_throws_on_write() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(DefaultPlacement::new("x", 42.0)); + let r = jsg::Rc::new(DefaultPlacement::new("x", 42)); ctx.set_global(lock, "obj", r); - let v: f64 = ctx.eval(lock, "obj.version").unwrap(); - assert!((v - 42.0).abs() < f64::EPSILON); + assert_eq!(ctx.eval::(lock, "obj.version").unwrap(), 42); let result = ctx.eval_raw(lock, "'use strict'; obj.version = 0"); assert!( result.is_err(), @@ -2203,7 +2189,7 @@ fn default_placement_readonly_throws_on_write() { fn default_placement_found_via_in_operator() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let r = jsg::Rc::new(DefaultPlacement::new("hi", 1.0)); + let r = jsg::Rc::new(DefaultPlacement::new("hi", 1)); ctx.set_global(lock, "obj", r); let found: bool = ctx.eval(lock, "'name' in obj").unwrap(); assert!(found, "prototype property must be found by 'in' operator"); diff --git a/src/rust/jsg-test/tests/resource_template.rs b/src/rust/jsg-test/tests/resource_template.rs index 296a909258a..aadeb26052c 100644 --- a/src/rust/jsg-test/tests/resource_template.rs +++ b/src/rust/jsg-test/tests/resource_template.rs @@ -19,7 +19,7 @@ use jsg_macros::jsg_static_constant; use crate::Harness; -type OptionalNumber = Option; +type OptionalNumber = Option; struct OptionalValue; @@ -46,7 +46,7 @@ struct Sample { #[jsg_resource(custom_members)] impl Sample { #[jsg_static_constant] - const VALUE: f64 = 1.0; + const VALUE: u32 = 1; #[jsg_constructor] fn constructor(this: String, _extra: OptionalNumber) -> Self { @@ -56,18 +56,18 @@ impl Sample { } #[jsg_method] - fn two(&self, a: Number, b: Number, _c: OptionalNumber) -> Number { - Number::new(a.value() + b.value()) + fn two(&self, a: u32, b: u32, _c: OptionalNumber) -> u32 { + a + b } #[jsg_method] - fn with_lock(&self, _lock: &mut jsg::Lock, a: Number) -> Number { + fn with_lock(&self, _lock: &mut jsg::Lock, a: u32) -> u32 { a } #[jsg_method] - fn add(a: Number, b: Number) -> Number { - Number::new(a.value() + b.value()) + fn add(a: u32, b: u32) -> u32 { + a + b } #[jsg_method] @@ -75,7 +75,7 @@ impl Sample { &self, _value: jsg::v8::Local<'s, jsg::v8::Value>, _optional: OptionalValue, - _after: jsg::Lenient, + _after: jsg::Lenient, ) -> bool { true } @@ -120,7 +120,7 @@ impl Sample { } #[jsg_static_constant(name = "EXTRA")] - const EXTRA_VALUE: f64 = 2.0; + const EXTRA_VALUE: u32 = 2; } impl Resource for Sample { @@ -228,7 +228,7 @@ fn constructor_called_without_new_throws_type_error() { for code in ["Sample('x')", "Sample.call({}, 'x')"] { let err = ctx - .eval::(lock, code) + .eval::(lock, code) .unwrap_err() .unwrap_jsg_err(lock); assert_eq!(err.name, jsg::ExceptionType::TypeError, "{code}"); diff --git a/src/rust/jsg-test/tests/resource_this.rs b/src/rust/jsg-test/tests/resource_this.rs index c07f8e78aa7..26d99534855 100644 --- a/src/rust/jsg-test/tests/resource_this.rs +++ b/src/rust/jsg-test/tests/resource_this.rs @@ -134,8 +134,8 @@ impl Queue { } #[jsg_method] - fn len(&self) -> f64 { - f64::from(u32::try_from(self.tasks.borrow().len()).unwrap()) + fn len(&self) -> u32 { + u32::try_from(self.tasks.borrow().len()).unwrap() } } diff --git a/src/rust/jsg-test/tests/string.rs b/src/rust/jsg-test/tests/string.rs index 2d2a3be22bf..f47e9ed4858 100644 --- a/src/rust/jsg-test/tests/string.rs +++ b/src/rust/jsg-test/tests/string.rs @@ -458,7 +458,7 @@ fn string_try_from_from_value() { fn string_try_from_fails_for_non_string() { let harness = crate::Harness::new(); harness.run_in_context(|lock, _ctx| { - let val: Local<'_, Value> = 42.0_f64.to_js(lock)?; + let val: Local<'_, Value> = 42_u32.to_js(lock)?; assert!(Local::::try_from(val).is_err()); Ok(()) }); diff --git a/src/rust/jsg-test/tests/unwrap.rs b/src/rust/jsg-test/tests/unwrap.rs index 2ce262ffcfd..440ed9b83a0 100644 --- a/src/rust/jsg-test/tests/unwrap.rs +++ b/src/rust/jsg-test/tests/unwrap.rs @@ -56,7 +56,7 @@ fn v8_type_checks_are_mutually_exclusive() { assert!(!b.is_string() && b.is_boolean() && !b.is_number()); // Number is only number - let n = 42.0_f64.to_js(lock)?; + let n = 42_i32.to_js(lock)?; assert!(!n.is_string() && !n.is_boolean() && n.is_number()); Ok(()) }); diff --git a/src/rust/jsg-test/tests/wildcard_property.rs b/src/rust/jsg-test/tests/wildcard_property.rs index fcf1769dcdc..7813a7f07b2 100644 --- a/src/rust/jsg-test/tests/wildcard_property.rs +++ b/src/rust/jsg-test/tests/wildcard_property.rs @@ -26,8 +26,8 @@ struct ProxyImpl { #[jsg_resource] impl ProxyImpl { #[jsg_property(prototype, readonly)] - pub fn get_bar(&self) -> f64 { - 123.0 + pub fn get_bar(&self) -> i32 { + 123 } #[jsg_wildcard_property] @@ -83,7 +83,7 @@ impl Caller { } #[jsg_wildcard_property] - pub fn get(&self, lock: &mut jsg::Lock, name: &str) -> Result, jsg::Error> { + pub fn get(&self, lock: &mut jsg::Lock, name: &str) -> Result, jsg::Error> { if name != "call" { return Ok(None); } @@ -104,7 +104,7 @@ struct Panicker { #[jsg_resource] impl Panicker { #[jsg_wildcard_property] - pub fn get(&self, name: &str) -> Option { + pub fn get(&self, name: &str) -> Option { assert!( name != self.trigger, "intentional panic in a wildcard property" diff --git a/src/rust/jsg/README.md b/src/rust/jsg/README.md index 018ee2bbf81..fbff207386f 100644 --- a/src/rust/jsg/README.md +++ b/src/rust/jsg/README.md @@ -562,7 +562,11 @@ that stays on the V8 heap is a `v8::Local<'s, v8::Number>`, which accepts only a Integers convert like C++ JSG's. `u8`, `u16`, `u32`, `i8`, `i16` and `i32` convert to a number, and from a value converted to a number, which must be in range. `i64` and `u64` convert to a `BigInt`, like `int64_t` and `uint64_t`, and from a `BigInt` that fits, or any other value converted -to a number, which must be finite and in range, and truncates toward zero. +to a number, which must be finite and in range, and truncates toward zero. An integer that stays +on the V8 heap is a `v8::Local<'s, v8::Int32>`, `v8::Uint32` or `v8::Integer` (either), which +accepts only an integral number in that range, as `v8::Local` accepts only a number. +`Local::int32_value`, `uint32_value` and `integer_value` convert any value with +JavaScript's `ToInt32`, `ToUint32` and `ToInteger`, as the V8 methods of those names do. Like C++ JSG's `kj::Date`, `SystemTime` converts to a `Date` of the milliseconds since the epoch, truncated toward zero, and from a `Date` or a number of milliseconds within the range of `kj::Date`. @@ -775,10 +779,10 @@ use jsg_macros::{jsg_resource, jsg_property, jsg_inspect_property}; impl Counter { // Prototype property (on the proto chain; use this in almost all cases) #[jsg_property(prototype)] - pub fn get_value(&self) -> f64 { /* ... */ } + pub fn get_value(&self) -> u32 { /* ... */ } #[jsg_property(prototype)] - pub fn set_value(&self, v: f64) { /* ... */ } + pub fn set_value(&self, v: u32) { /* ... */ } // Read-only prototype property (no paired `set_value`) #[jsg_property(prototype, readonly)] @@ -879,6 +883,9 @@ Each nested type's constructor is a property of the prototype, named after the t ## Fast API `#[jsg_method]` adds a V8 Fast API callback for the types in `fast_api.rs`; it must not call into V8. +An integer argument reaches the callback as a `double` and converts with the slow path's checks; +an integer of up to 32 bits returns as an `int32_t` or a `uint32_t`, while a method that returns an +`i64` or a `u64` has no fast callback. ## FFI Functions with Raw Pointers diff --git a/src/rust/jsg/dict.rs b/src/rust/jsg/dict.rs index 64599852614..b55cc410ba0 100644 --- a/src/rust/jsg/dict.rs +++ b/src/rust/jsg/dict.rs @@ -29,8 +29,8 @@ /// } /// /// #[jsg_method] -/// pub fn get_counts(&self) -> Dict { -/// Dict::from(vec![("hits".to_owned(), 3.0)]) +/// pub fn get_counts(&self) -> Dict { +/// Dict::from(vec![("hits".to_owned(), 3)]) /// } /// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/src/rust/jsg/fast_api.rs b/src/rust/jsg/fast_api.rs index 8aafa41163d..b2ede65d8df 100644 --- a/src/rust/jsg/fast_api.rs +++ b/src/rust/jsg/fast_api.rs @@ -68,25 +68,28 @@ impl FastApiArg for f64 { } } -// Integers come from V8 as doubles, so that their range checks and truncation are the slow path's. +// Integers come from V8 as doubles, so that their range checks and truncation are the slow +// path's. V8's own integer argument types convert differently: by default with `ToInt32`'s +// wrapping, and with `kEnforceRangeBit` they truncate before the range check, so that +// `2 ** 31 - 0.5` is an `int32_t` and `-0.5` a `uint32_t`, where C++ JSG and `from_number` +// check the range first and throw a `TypeError` for both. -impl private::Sealed for i32 {} -impl FastApiArg for i32 { - type Raw = f64; - const TYPE: FastApiType = FastApiType::Float64; - fn from_raw(raw: f64) -> Result { - Self::from_number(raw) - } +macro_rules! impl_fast_api_integer_arg { + ($($type:ty),*) => { + $( + impl private::Sealed for $type {} + impl FastApiArg for $type { + type Raw = f64; + const TYPE: FastApiType = FastApiType::Float64; + fn from_raw(raw: f64) -> Result { + Self::from_number(raw) + } + } + )* + }; } -impl private::Sealed for u32 {} -impl FastApiArg for u32 { - type Raw = f64; - const TYPE: FastApiType = FastApiType::Float64; - fn from_raw(raw: f64) -> Result { - Self::from_number(raw) - } -} +impl_fast_api_integer_arg!(i8, i16, i32, u8, u16, u32); // Return values @@ -106,6 +109,32 @@ impl FastApiReturn for f64 { } } +// Integers go back to V8 as its `int32_t` or `uint32_t`, which it turns into the same number +// as `Integer::New` does on the slow path. The narrower types widen to one of the two. + +macro_rules! impl_fast_api_integer_return { + ($($type:ty => $raw:ty, $variant:ident);* $(;)?) => { + $( + impl FastApiReturn for $type { + type Raw = $raw; + const TYPE: FastApiType = FastApiType::$variant; + fn into_raw(self) -> $raw { + self.into() + } + } + )* + }; +} + +impl_fast_api_integer_return!( + i8 => i32, Int32; + i16 => i32, Int32; + i32 => i32, Int32; + u8 => u32, Uint32; + u16 => u32, Uint32; + u32 => u32, Uint32; +); + impl private::Sealed for () {} impl FastApiReturn for () { type Raw = Self; diff --git a/src/rust/jsg/ffi.c++ b/src/rust/jsg/ffi.c++ index 626cdec9399..eb9b42d2b46 100644 --- a/src/rust/jsg/ffi.c++ +++ b/src/rust/jsg/ffi.c++ @@ -168,6 +168,14 @@ bool local_is_number(const Local& val) { return local_as_ref_from_ffi(val)->IsNumber(); } +bool local_is_int32(const Local& val) { + return local_as_ref_from_ffi(val)->IsInt32(); +} + +bool local_is_uint32(const Local& val) { + return local_as_ref_from_ffi(val)->IsUint32(); +} + bool local_is_null(const Local& val) { return local_as_ref_from_ffi(val)->IsNull(); } @@ -287,6 +295,24 @@ bool local_is_shared_array_buffer(const Local& val) { return ::rust::String(*utf8, utf8.length()); } +bool local_value_int32_value(Isolate* isolate, Local value, int32_t& result) { + return local_from_ffi(kj::mv(value)) + ->Int32Value(isolate->GetCurrentContext()) + .To(&result); +} + +bool local_value_uint32_value(Isolate* isolate, Local value, uint32_t& result) { + return local_from_ffi(kj::mv(value)) + ->Uint32Value(isolate->GetCurrentContext()) + .To(&result); +} + +bool local_value_integer_value(Isolate* isolate, Local value, int64_t& result) { + return local_from_ffi(kj::mv(value)) + ->IntegerValue(isolate->GetCurrentContext()) + .To(&result); +} + // Utf8Value Utf8Value utf8_value_new(Isolate* isolate, Local value) { auto* v = new v8::String::Utf8Value(isolate, local_from_ffi(kj::mv(value))); @@ -428,6 +454,27 @@ double local_number_value(const Local& value) { return local_as_ref_from_ffi(value)->Value(); } +// Local +Local integer_new(Isolate* isolate, int32_t value) { + return to_ffi(v8::Integer::New(isolate, value)); +} + +Local integer_new_from_unsigned(Isolate* isolate, uint32_t value) { + return to_ffi(v8::Integer::NewFromUnsigned(isolate, value)); +} + +int64_t local_integer_value(const Local& value) { + return local_as_ref_from_ffi(value)->Value(); +} + +int32_t local_int32_value(const Local& value) { + return local_as_ref_from_ffi(value)->Value(); +} + +uint32_t local_uint32_value(const Local& value) { + return local_as_ref_from_ffi(value)->Value(); +} + // Local Local big_int_new(Isolate* isolate, int64_t value) { return to_ffi(v8::BigInt::New(isolate, value)); @@ -1188,6 +1235,10 @@ v8::CTypeInfo cTypeOf(FastApiType type) { return v8::CTypeInfo(v8::CTypeInfo::Type::kVoid); case FastApiType::Bool: return v8::CTypeInfo(v8::CTypeInfo::Type::kBool); + case FastApiType::Int32: + return v8::CTypeInfo(v8::CTypeInfo::Type::kInt32); + case FastApiType::Uint32: + return v8::CTypeInfo(v8::CTypeInfo::Type::kUint32); case FastApiType::Float64: return v8::CTypeInfo(v8::CTypeInfo::Type::kFloat64); } diff --git a/src/rust/jsg/ffi.h b/src/rust/jsg/ffi.h index 4ff0a817683..ac997d6a5cc 100644 --- a/src/rust/jsg/ffi.h +++ b/src/rust/jsg/ffi.h @@ -135,6 +135,8 @@ bool local_is_compatible_with_isolate(Isolate* isolate, const Local& value); bool local_is_string(const Local& val); bool local_is_boolean(const Local& val); bool local_is_number(const Local& val); +bool local_is_int32(const Local& val); +bool local_is_uint32(const Local& val); bool local_is_null(const Local& val); bool local_is_undefined(const Local& val); bool local_is_null_or_undefined(const Local& val); @@ -164,6 +166,12 @@ bool local_is_name(const Local& val); bool local_is_big_int(const Local& val); bool local_is_date(const Local& val); ::rust::String local_type_of(Isolate* isolate, const Local& val); +// Coerce with ToInt32, ToUint32 and ToInteger (v8::Value::Int32Value() and the like): store the +// result in `result` and return true, or return false if the coercion threw, leaving the exception +// pending. +bool local_value_int32_value(Isolate* isolate, Local value, int32_t& result); +bool local_value_uint32_value(Isolate* isolate, Local value, uint32_t& result); +bool local_value_integer_value(Isolate* isolate, Local value, int64_t& result); // Local Local local_string_empty(Isolate* isolate); @@ -210,6 +218,13 @@ bool local_boolean_value(const Local& value); // Local double local_number_value(const Local& value); +// Local +Local integer_new(Isolate* isolate, int32_t value); +Local integer_new_from_unsigned(Isolate* isolate, uint32_t value); +int64_t local_integer_value(const Local& value); +int32_t local_int32_value(const Local& value); +uint32_t local_uint32_value(const Local& value); + // Local Local big_int_new(Isolate* isolate, int64_t value); Local big_int_new_from_unsigned(Isolate* isolate, uint64_t value); diff --git a/src/rust/jsg/v8.rs b/src/rust/jsg/v8.rs index 5ce0a281c6d..93c3c115976 100644 --- a/src/rust/jsg/v8.rs +++ b/src/rust/jsg/v8.rs @@ -282,6 +282,8 @@ pub mod ffi { pub unsafe fn local_is_string(value: &Local) -> bool; pub unsafe fn local_is_boolean(value: &Local) -> bool; pub unsafe fn local_is_number(value: &Local) -> bool; + pub unsafe fn local_is_int32(value: &Local) -> bool; + pub unsafe fn local_is_uint32(value: &Local) -> bool; pub unsafe fn local_is_null(value: &Local) -> bool; pub unsafe fn local_is_undefined(value: &Local) -> bool; pub unsafe fn local_is_null_or_undefined(value: &Local) -> bool; @@ -311,6 +313,24 @@ pub mod ffi { pub unsafe fn local_is_big_int(value: &Local) -> bool; pub unsafe fn local_is_date(value: &Local) -> bool; pub unsafe fn local_type_of(isolate: *mut Isolate, value: &Local) -> String; + // Coerce with ToInt32, ToUint32 and ToInteger (`v8::Value::Int32Value()` and the like): + // store the result in `result` and return true, or return false if the coercion threw, + // leaving the exception pending; see `call_into_js()`. + pub unsafe fn local_value_int32_value( + isolate: *mut Isolate, + value: Local, + result: &mut i32, + ) -> bool; + pub unsafe fn local_value_uint32_value( + isolate: *mut Isolate, + value: Local, + result: &mut u32, + ) -> bool; + pub unsafe fn local_value_integer_value( + isolate: *mut Isolate, + value: Local, + result: &mut i64, + ) -> bool; // Local pub unsafe fn local_string_length(value: &Local) -> i32; @@ -385,6 +405,13 @@ pub mod ffi { // Local pub unsafe fn local_number_value(value: &Local) -> f64; + // Local + pub unsafe fn integer_new(isolate: *mut Isolate, value: i32) -> Local; + pub unsafe fn integer_new_from_unsigned(isolate: *mut Isolate, value: u32) -> Local; + pub unsafe fn local_integer_value(value: &Local) -> i64; + pub unsafe fn local_int32_value(value: &Local) -> i32; + pub unsafe fn local_uint32_value(value: &Local) -> u32; + // Local pub unsafe fn big_int_new(isolate: *mut Isolate, value: i64) -> Local; pub unsafe fn big_int_new_from_unsigned(isolate: *mut Isolate, value: u64) -> Local; @@ -704,10 +731,15 @@ pub mod ffi { length: u32, } + /// The C types of a fast callback's arguments and result. `Int32` and `Uint32` are result + /// types only: an integer argument is a `Float64`, converted with the slow path's checks + /// (see `fast_api.rs`). #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum FastApiType { Void, Bool, + Int32, + Uint32, Float64, } @@ -879,7 +911,7 @@ impl std::fmt::Display for ffi::ExceptionType { // between them are in `impl_local_cast!`. // // Deliberately left unimplemented because we didn't need them yet: -// - Primitive, Numeric, Integer, Int32, Uint32 +// - Primitive, Numeric // - NumberObject, BooleanObject, StringObject, SymbolObject, BigIntObject // - Map, Set, Proxy, RegExp, External, DataView, SharedArrayBuffer, Float16Array // - WasmModuleObject, WasmMemoryObject @@ -895,6 +927,20 @@ pub struct Boolean; /// this is the handle to one on the V8 heap. #[derive(Debug)] pub struct Number; +/// Marker for `v8::Integer` handles: an integral number in the range of `i32` or `u32`. +/// +/// That is what `Integer::new` and `Integer::new_from_unsigned` create, and what the Rust +/// integer types of up to 32 bits convert to; `i64` and `u64` convert to a `BigInt`. V8's own +/// cast to `Integer` accepts any number; this one, like `rusty_v8`'s, accepts only a value that is +/// an `Int32` or a `Uint32`. +#[derive(Debug)] +pub struct Integer; +/// Marker for `v8::Int32` handles: an integral number in the range of `i32`. `-0` is not one. +#[derive(Debug)] +pub struct Int32; +/// Marker for `v8::Uint32` handles: an integral number in the range of `u32`. `-0` is not one. +#[derive(Debug)] +pub struct Uint32; /// Marker for `v8::Name` handles (supertype of `String` and `Symbol`). #[derive(Debug)] pub struct Name; @@ -1447,6 +1493,29 @@ impl<'s, T> Local<'s, T> { unsafe { ffi::local_is_number(self.raw()) } } + /// Returns true if the value is an integral number in the range of `i32`, other than `-0`. + /// + /// Corresponds to `v8::Value::IsInt32()`. + pub fn is_int32(&self) -> bool { + // SAFETY: handle is valid within the current HandleScope. + unsafe { ffi::local_is_int32(self.raw()) } + } + + /// Returns true if the value is an integral number in the range of `u32`, other than `-0`. + /// + /// Corresponds to `v8::Value::IsUint32()`. + pub fn is_uint32(&self) -> bool { + // SAFETY: handle is valid within the current HandleScope. + unsafe { ffi::local_is_uint32(self.raw()) } + } + + /// Whether the value is a `v8::Integer`, which has no `Is*` test of its own: an integral + /// number in the range of `i32` or `u32`, as `Integer::new` and `Integer::new_from_unsigned` + /// create. + fn is_int32_or_uint32(self) -> bool { + self.is_int32() || self.is_uint32() + } + pub fn is_null(&self) -> bool { // SAFETY: handle is valid within the current HandleScope. unsafe { ffi::local_is_null(self.raw()) } @@ -1637,6 +1706,63 @@ impl<'s, T> Local<'s, T> { // HandleScope. unsafe { ffi::local_type_of(lock.isolate().as_ffi(), self.raw()) } } + + /// Converts the value with JavaScript's `ToInt32`: `ToNumber`, then truncation toward zero + /// modulo 2^32, so NaN and the infinities are 0 and `2 ** 32 + 5` is 5. If the coercion + /// throws, or execution is terminating, fails with an error for which + /// [`Error::is_pending`] is true. + /// + /// Corresponds to `v8::Value::Int32Value()`. + pub fn int32_value(&self, lock: &mut Lock<'s>) -> Result { + call_into_js(lock, |lock| { + let mut result = 0; + // SAFETY: `lock` guarantees the isolate is locked; handle is valid within the + // current HandleScope. + let ok = unsafe { + ffi::local_value_int32_value(lock.isolate().as_ffi(), self.into_ffi(), &mut result) + }; + Ok(ok.then_some(result)) + }) + } + + /// Converts the value with JavaScript's `ToUint32`: `ToNumber`, then truncation toward zero + /// modulo 2^32, so NaN and the infinities are 0 and `-1` is `u32::MAX`. If the coercion + /// throws, or execution is terminating, fails with an error for which + /// [`Error::is_pending`] is true. + /// + /// Corresponds to `v8::Value::Uint32Value()`. + pub fn uint32_value(&self, lock: &mut Lock<'s>) -> Result { + call_into_js(lock, |lock| { + let mut result = 0; + // SAFETY: `lock` guarantees the isolate is locked; handle is valid within the + // current HandleScope. + let ok = unsafe { + ffi::local_value_uint32_value(lock.isolate().as_ffi(), self.into_ffi(), &mut result) + }; + Ok(ok.then_some(result)) + }) + } + + /// Converts the value with JavaScript's `ToInteger` (`ToNumber`, then truncation toward + /// zero, with NaN as 0), saturating at the range of `i64`. If the coercion throws, or + /// execution is terminating, fails with an error for which [`Error::is_pending`] is true. + /// + /// Corresponds to `v8::Value::IntegerValue()`. + pub fn integer_value(&self, lock: &mut Lock<'s>) -> Result { + call_into_js(lock, |lock| { + let mut result = 0; + // SAFETY: `lock` guarantees the isolate is locked; handle is valid within the + // current HandleScope. + let ok = unsafe { + ffi::local_value_integer_value( + lock.isolate().as_ffi(), + self.into_ffi(), + &mut result, + ) + }; + Ok(ok.then_some(result)) + }) + } } impl PartialEq for Local<'_, Value> { @@ -1950,6 +2076,9 @@ macro_rules! impl_local_cast { impl_local_cast!(Boolean: Value; is_boolean); impl_local_cast!(Number: Value; is_number); +impl_local_cast!(Integer: Number, Value; is_int32_or_uint32); +impl_local_cast!(Int32: Integer, Number, Value; is_int32); +impl_local_cast!(Uint32: Integer, Number, Value; is_uint32); impl_local_cast!(Name: Value; is_name); impl_local_cast!(String: Name, Value; is_string); impl_local_cast!(Symbol: Name, Value; is_symbol); @@ -2814,6 +2943,9 @@ impl_local_from_js!(ArrayBuffer, is_array_buffer, "ArrayBuffer"); impl_local_from_js!(ArrayBufferView, is_array_buffer_view, "ArrayBufferView"); impl_local_from_js!(Boolean, is_boolean, "Boolean"); impl_local_from_js!(Number, is_number, "Number"); +impl_local_from_js!(Integer, is_int32_or_uint32, "Integer"); +impl_local_from_js!(Int32, is_int32, "Int32"); +impl_local_from_js!(Uint32, is_uint32, "Uint32"); impl_typed_array!(Uint8Array, u8, local_uint8_array_get); impl_typed_array!(Uint16Array, u16, local_uint16_array_get); @@ -3192,6 +3324,63 @@ impl Local<'_, Number> { } } +// ============================================================================= +// `Integer`, `Int32` and `Uint32` +// ============================================================================= + +impl Integer { + /// Creates a number from an `i32`. + /// + /// Corresponds to `v8::Integer::New()`. + pub fn new<'s>(lock: &mut crate::Lock<'s>, value: i32) -> Local<'s, Self> { + let isolate = lock.isolate(); + // SAFETY: Lock guarantees the isolate is locked and a HandleScope is active. + unsafe { Local::from_ffi(ffi::integer_new(isolate.as_ffi(), value)) } + } + + /// Creates a number from a `u32`. + /// + /// Corresponds to `v8::Integer::NewFromUnsigned()`. + pub fn new_from_unsigned<'s>(lock: &mut crate::Lock<'s>, value: u32) -> Local<'s, Self> { + let isolate = lock.isolate(); + // SAFETY: Lock guarantees the isolate is locked and a HandleScope is active. + unsafe { Local::from_ffi(ffi::integer_new_from_unsigned(isolate.as_ffi(), value)) } + } +} + +impl Local<'_, Integer> { + /// The integer this handle refers to. + /// + /// Corresponds to `v8::Integer::Value()`. + #[inline] + pub fn value(&self) -> i64 { + // SAFETY: the handle is a valid V8 Integer handle. + unsafe { ffi::local_integer_value(self.raw()) } + } +} + +impl Local<'_, Int32> { + /// The integer this handle refers to. + /// + /// Corresponds to `v8::Int32::Value()`. + #[inline] + pub fn value(&self) -> i32 { + // SAFETY: the handle is a valid V8 Int32 handle. + unsafe { ffi::local_int32_value(self.raw()) } + } +} + +impl Local<'_, Uint32> { + /// The integer this handle refers to. + /// + /// Corresponds to `v8::Uint32::Value()`. + #[inline] + pub fn value(&self) -> u32 { + // SAFETY: the handle is a valid V8 Uint32 handle. + unsafe { ffi::local_uint32_value(self.raw()) } + } +} + // ============================================================================= // `Name` — supertype of `String` and `Symbol` // ============================================================================= diff --git a/src/rust/jsg/wrappable.rs b/src/rust/jsg/wrappable.rs index 98aff889990..0efa5fba13a 100644 --- a/src/rust/jsg/wrappable.rs +++ b/src/rust/jsg/wrappable.rs @@ -54,8 +54,8 @@ //! //! ## Integers //! -//! The integer types (`u8`, `u16`, `u32`, `i8`, `i16`, `i32`) convert to a `number`, and from -//! JavaScript like C++ JSG's integers: +//! The integer types (`u8`, `u16`, `u32`, `i8`, `i16`, `i32`) convert to a `number` with +//! `Integer::New`, as C++ JSG's do, and from JavaScript like C++ JSG's integers: //! //! - Values out of the type's range, checked before truncation, are a //! `TypeError` (e.g., `256` or `255.5` for `u8`) @@ -166,10 +166,13 @@ impl_traced_noop!( crate::v8::Float64Array, crate::v8::Int8Array, crate::v8::Int16Array, + crate::v8::Int32, crate::v8::Int32Array, + crate::v8::Integer, crate::v8::Number, crate::v8::Uint8Array, crate::v8::Uint16Array, + crate::v8::Uint32, crate::v8::Uint32Array, &str, ); @@ -658,10 +661,10 @@ impl<'s, T: Type + FromJS<'s, ResultType = T>> FromJS<'s> for &[T] { } // Integer types follow C++ JSG (jsg/value.h). To JavaScript, an integer of up -// to 32 bits is a number, and an i64 or u64 is a BigInt. From JavaScript, a value out -// of the type's range, checked before truncation, is a TypeError, and so is a -// non-finite one, except that i32 reads it as 0. In-range values truncate -// toward zero. +// to 32 bits is a number, made with `Integer::New` as in C++ JSG, and an i64 or +// u64 is a BigInt. From JavaScript, a value out of the type's range, checked +// before truncation, is a TypeError, and so is a non-finite one, except that i32 +// reads it as 0. In-range values truncate toward zero. /// Converts a JavaScript number to an integer type. Fast callbacks use it too, so that both paths /// convert alike. pub trait FromNumber: Sized { @@ -669,7 +672,7 @@ pub trait FromNumber: Sized { } macro_rules! impl_integer { - ($($type:ty => $name:literal, $non_finite:expr, $range:expr);* $(;)?) => { + ($($type:ty => $name:literal, $new:ident, $non_finite:expr, $range:expr);* $(;)?) => { $( impl Type for $type { fn class_name() -> &'static str { @@ -684,7 +687,7 @@ macro_rules! impl_integer { impl<'s> ToJS<'s> for $type { #[inline] fn to_js(self, lock: &mut Lock<'s>) -> Result, Error> { - f64::from(self).to_js(lock) + Ok(v8::Integer::$new(lock, self.into()).into()) } } @@ -769,12 +772,12 @@ fn signed_range(min: T, max: T) -> String { } impl_integer!( - u8 => "octet", not_an_integer(), unsigned_range(u8::MAX); - u16 => "unsigned short integer", not_an_integer(), unsigned_range(u16::MAX); - u32 => "unsigned integer", not_an_integer(), unsigned_range(u32::MAX); - i8 => "byte", not_an_integer(), signed_range(i8::MIN, i8::MAX); - i16 => "short integer", not_an_integer(), signed_range(i16::MIN, i16::MAX); - i32 => "integer", Ok(0), signed_range(i32::MIN, i32::MAX); + u8 => "octet", new_from_unsigned, not_an_integer(), unsigned_range(u8::MAX); + u16 => "unsigned short integer", new_from_unsigned, not_an_integer(), unsigned_range(u16::MAX); + u32 => "unsigned integer", new_from_unsigned, not_an_integer(), unsigned_range(u32::MAX); + i8 => "byte", new, not_an_integer(), signed_range(i8::MIN, i8::MAX); + i16 => "short integer", new, not_an_integer(), signed_range(i16::MIN, i16::MAX); + i32 => "integer", new, Ok(0), signed_range(i32::MIN, i32::MAX); ); impl Type for i64 {