Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce NonMaxU32 for array index #2131

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions boa_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub mod class;
pub mod context;
pub mod environments;
pub mod job;
pub mod nonmaxu32;
pub mod object;
pub mod property;
pub mod realm;
Expand Down
180 changes: 180 additions & 0 deletions boa_engine/src/nonmaxu32.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! This module implements `NonMaxU32`
//! which is known not to equal `u32::MAX`.
//!
//! This would be useful for integers like `https://tc39.es/ecma262/#array-index`.

use boa_gc::{unsafe_empty_trace, Finalize, Trace};
use std::fmt;
use std::ops::{BitOr, BitOrAssign};

/// An integer that is known not to equal `u32::MAX`.
///
/// This enables some memory layout optimization.
/// For example, `Option<NonMaxU32>` is the same size as `u32`:
///
/// ```rust
/// use std::mem::size_of;
/// use boa_engine::nonmaxu32::NonMaxU32;
/// assert_eq!(size_of::<Option<NonMaxU32>>(), size_of::<u32>());
/// ```
#[derive(Copy, Clone, Eq, Finalize, PartialEq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
pub struct NonMaxU32(u32);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest the internal type to be not an enum type and be a structure type:

pub struct NonMaxU32 {
    inner: u32,
}

This would disallow direct editing of the internal structure (having to use getters and setters), and changing the internal type would be API compatible.

Talking about changing the internal type, it might be useful to use a NonZeroU32, which is better optimized for space. On the other hand, this would require using a +1 when storing, and a -1 when retrieving, which could cause a small performance penalty. We should test both options.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for late reply 🙏 Thank you for your suggestion! I will try to work on a fix.

On the other hand, this would require using a +1 when storing, and a -1 when retrieving, which could cause a small performance penalty.

This sounds a good idea to me. I experimented a bit using XOR to save with NonZeroU32 like the nonmax crate does last week but I realized we would need to rev() the iterator because the xor'ed value would be in reversed order. So, I wonder just +1 and -1 would be easier.

I will try it more and share more information here or on Discord 🙏 Thank you!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm ok with either approach (-1/+1 or XOR).


// Safety: `NonMaxU32` does not contain any objects which needs to be traced,
// so this is safe.
unsafe impl Trace for NonMaxU32 {
unsafe_empty_trace!();
}
Comment on lines +24 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to just not implement this, and use #[unsafe_ignore_trace] in the enum (if that's allowed). It should be more performant.


/// An error type returned when a checked integral type conversion fails (mimics [`std::num::TryFromIntError`])
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TryFromIntError(());

impl fmt::Display for TryFromIntError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
"out of range integral type conversion attempted".fmt(fmt)
}
}

impl From<core::num::TryFromIntError> for TryFromIntError {
fn from(_: core::num::TryFromIntError) -> Self {
Self(())
}
}

impl From<core::convert::Infallible> for TryFromIntError {
fn from(never: core::convert::Infallible) -> Self {
match never {}
}
}

/// An error type returned when an integer cannot be parsed (mimics [`std::num::ParseIntError`])
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ParseIntError(());

impl fmt::Display for ParseIntError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
"unable to parse integer".fmt(fmt)
}
}

impl From<core::num::ParseIntError> for ParseIntError {
fn from(_: core::num::ParseIntError) -> Self {
Self(())
}
}

impl NonMaxU32 {
/// Creates a non-u32-max without checking the value.
///
/// # Safety
///
/// The value must not be `u32::MAX`.
#[inline]
pub const unsafe fn new_unchecked(n: u32) -> Self {
// SAFETY: this is guaranteed to be safe by the caller.
Self(n)
}

/// Creates a non-u32-max if the given value is not `u32::MAX`.
#[inline]
pub const fn new(n: u32) -> Option<Self> {
if n == u32::MAX {
None
} else {
// SAFETY: we just checked that there's no `u32::MAX`
Some(Self(n))
}
}

/// Returns the value as a primitive type.
#[inline]
pub const fn get(self) -> u32 {
self.0
}
Comment on lines +93 to +95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also include setters? And operations such as add, sub, mul and so on?

Copy link
Contributor Author

@CYBAI CYBAI Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with adding these methods though, I wonder it also depends on how we want to use it. Do we want to always do calculation with NonMaxU32 directly?

On the other hand, I wonder it might be good to at least add methods like checked_add or saturating_add on NonZeroU32.

WDYT?

}

impl TryFrom<u32> for NonMaxU32 {
type Error = TryFromIntError;

fn try_from(value: u32) -> Result<Self, Self::Error> {
Self::new(value).ok_or(TryFromIntError(()))
}
}

impl From<NonMaxU32> for u32 {
/// Converts a `NonMaxU32` into an `u32`
fn from(nonzero: NonMaxU32) -> Self {
nonzero.0
}
}

impl core::str::FromStr for NonMaxU32 {
type Err = ParseIntError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(u32::from_str(value)?).ok_or(ParseIntError(()))
}
}

impl BitOr for NonMaxU32 {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
// Safety: since `self` and `rhs` are both nonzero, the
// result of the bitwise-or will be nonzero.
unsafe { NonMaxU32::new_unchecked(self.get() | rhs.get()) }
}
}

impl BitOr<u32> for NonMaxU32 {
type Output = Self;

#[inline]
fn bitor(self, rhs: u32) -> Self::Output {
// Safety: since `self` is nonzero, the result of the
// bitwise-or will be nonzero regardless of the value of
// `rhs`.
unsafe { NonMaxU32::new_unchecked(self.get() | rhs) }
}
}

impl BitOr<NonMaxU32> for u32 {
type Output = NonMaxU32;

#[inline]
fn bitor(self, rhs: NonMaxU32) -> Self::Output {
// Safety: since `rhs` is nonzero, the result of the
// bitwise-or will be nonzero regardless of the value of
// `self`.
unsafe { NonMaxU32::new_unchecked(self | rhs.get()) }
}
}
Comment on lines +120 to +152

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Safety" argument and code seems to be copied from the nonzero implementaion and is not correct. (At least the comment is wrong)


impl BitOrAssign for NonMaxU32 {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = *self | rhs;
}
}

impl BitOrAssign<u32> for NonMaxU32 {
#[inline]
fn bitor_assign(&mut self, rhs: u32) {
*self = *self | rhs;
}
}

impl fmt::Debug for NonMaxU32 {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
Comment on lines +168 to +173
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a good implementation for Display, but I would say the Debug implementation shouldn't hide the internal structure. I'm open to suggestions, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to follow NonZeroU32 debug though, it sounds fine to me to format it like NonMaxU32 { inner: 42 }.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Razican


impl fmt::Display for NonMaxU32 {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
12 changes: 6 additions & 6 deletions boa_engine/src/object/internal_methods/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub(crate) fn arguments_exotic_get_own_property(
.borrow()
.as_mapped_arguments()
.expect("arguments exotic method must only be callable from arguments objects")
.get(*index as usize)
.get(index.get() as usize)
{
// a. Set desc.[[Value]] to Get(map, P).
return Ok(Some(
Expand Down Expand Up @@ -81,8 +81,8 @@ pub(crate) fn arguments_exotic_define_own_property(
obj.borrow()
.as_mapped_arguments()
.expect("arguments exotic method must only be callable from arguments objects")
.get(index as usize)
.map(|value| (index as usize, value))
.get(index.get() as usize)
.map(|value| (index.get() as usize, value))
} else {
None
};
Expand Down Expand Up @@ -173,7 +173,7 @@ pub(crate) fn arguments_exotic_get(
.borrow()
.as_mapped_arguments()
.expect("arguments exotic method must only be callable from arguments objects")
.get(*index as usize)
.get(index.get() as usize)
{
// a. Assert: map contains a formal parameter mapping for P.
// b. Return Get(map, P).
Expand Down Expand Up @@ -212,7 +212,7 @@ pub(crate) fn arguments_exotic_set(
obj.borrow_mut()
.as_mapped_arguments_mut()
.expect("arguments exotic method must only be callable from arguments objects")
.set(index as usize, &value);
.set(index.get() as usize, &value);
}
}

Expand Down Expand Up @@ -243,7 +243,7 @@ pub(crate) fn arguments_exotic_delete(
obj.borrow_mut()
.as_mapped_arguments_mut()
.expect("arguments exotic method must only be callable from arguments objects")
.delete(*index as usize);
.delete(index.get() as usize);
}
}

Expand Down
8 changes: 6 additions & 2 deletions boa_engine/src/object/internal_methods/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ pub(crate) fn array_exotic_define_own_property(
array_set_length(obj, desc, context)
}
// 3. Else if P is an array index, then
PropertyKey::Index(index) if index < u32::MAX => {
PropertyKey::Index(index) => {
let index = index.get();

// a. Let oldLenDesc be OrdinaryGetOwnProperty(A, "length").
let old_len_desc = super::ordinary_get_own_property(obj, &"length".into(), context)?
.expect("the property descriptor must exist");
Expand Down Expand Up @@ -201,14 +203,16 @@ fn array_set_length(
.borrow()
.properties
.index_property_keys()
.filter(|idx| new_len <= **idx && **idx < u32::MAX)
.filter(|idx| new_len <= idx.get())
.copied()
.collect();
keys.sort_unstable_by(|x, y| y.cmp(x));
keys
};

for index in ordered_keys {
let index = index.get();

// a. Let deleteSucceeded be ! A.[[Delete]](P).
// b. If deleteSucceeded is false, then
if !obj.__delete__(&index.into(), context)? {
Expand Down
33 changes: 18 additions & 15 deletions boa_engine/src/object/internal_methods/integer_indexed.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
builtins::{array_buffer::SharedMemoryOrder, typed_array::integer_indexed_object::ContentType},
nonmaxu32::NonMaxU32,
object::JsObject,
property::{PropertyDescriptor, PropertyKey},
Context, JsResult, JsValue,
Expand Down Expand Up @@ -44,14 +45,16 @@ pub(crate) fn integer_indexed_exotic_get_own_property(
// i. Let value be ! IntegerIndexedElementGet(O, numericIndex).
// ii. If value is undefined, return undefined.
// iii. Return the PropertyDescriptor { [[Value]]: value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }.
Ok(integer_indexed_element_get(obj, *index as usize).map(|v| {
PropertyDescriptor::builder()
.value(v)
.writable(true)
.enumerable(true)
.configurable(true)
.build()
}))
Ok(
integer_indexed_element_get(obj, index.get() as usize).map(|v| {
PropertyDescriptor::builder()
.value(v)
.writable(true)
.enumerable(true)
.configurable(true)
.build()
}),
)
} else {
// 2. Return OrdinaryGetOwnProperty(O, P).
super::ordinary_get_own_property(obj, key, context)
Expand All @@ -74,7 +77,7 @@ pub(crate) fn integer_indexed_exotic_has_property(
// a. Let numericIndex be ! CanonicalNumericIndexString(P).
if let PropertyKey::Index(index) = key {
// b. If numericIndex is not undefined, return ! IsValidIntegerIndex(O, numericIndex).
Ok(is_valid_integer_index(obj, *index as usize))
Ok(is_valid_integer_index(obj, index.get() as usize))
} else {
// 2. Return ? OrdinaryHasProperty(O, P).
super::ordinary_has_property(obj, key, context)
Expand Down Expand Up @@ -103,7 +106,7 @@ pub(crate) fn integer_indexed_exotic_define_own_property(
// iii. If Desc has an [[Enumerable]] field and if Desc.[[Enumerable]] is false, return false.
// v. If Desc has a [[Writable]] field and if Desc.[[Writable]] is false, return false.
// iv. If ! IsAccessorDescriptor(Desc) is true, return false.
if !is_valid_integer_index(obj, index as usize)
if !is_valid_integer_index(obj, index.get() as usize)
|| !desc
.configurable()
.or_else(|| desc.enumerable())
Expand All @@ -116,7 +119,7 @@ pub(crate) fn integer_indexed_exotic_define_own_property(

// vi. If Desc has a [[Value]] field, perform ? IntegerIndexedElementSet(O, numericIndex, Desc.[[Value]]).
if let Some(value) = desc.value() {
integer_indexed_element_set(obj, index as usize, value, context)?;
integer_indexed_element_set(obj, index.get() as usize, value, context)?;
}

// vii. Return true.
Expand Down Expand Up @@ -145,7 +148,7 @@ pub(crate) fn integer_indexed_exotic_get(
// b. If numericIndex is not undefined, then
if let PropertyKey::Index(index) = key {
// i. Return ! IntegerIndexedElementGet(O, numericIndex).
Ok(integer_indexed_element_get(obj, *index as usize).unwrap_or_default())
Ok(integer_indexed_element_get(obj, index.get() as usize).unwrap_or_default())
} else {
// 2. Return ? OrdinaryGet(O, P, Receiver).
super::ordinary_get(obj, key, receiver, context)
Expand All @@ -171,7 +174,7 @@ pub(crate) fn integer_indexed_exotic_set(
// b. If numericIndex is not undefined, then
if let PropertyKey::Index(index) = key {
// i. Perform ? IntegerIndexedElementSet(O, numericIndex, V).
integer_indexed_element_set(obj, index as usize, &value, context)?;
integer_indexed_element_set(obj, index.get() as usize, &value, context)?;

// ii. Return true.
Ok(true)
Expand All @@ -198,7 +201,7 @@ pub(crate) fn integer_indexed_exotic_delete(
// b. If numericIndex is not undefined, then
if let PropertyKey::Index(index) = key {
// i. If ! IsValidIntegerIndex(O, numericIndex) is false, return true; else return false.
Ok(!is_valid_integer_index(obj, *index as usize))
Ok(!is_valid_integer_index(obj, index.get() as usize))
} else {
// 2. Return ? OrdinaryDelete(O, P).
super::ordinary_delete(obj, key, context)
Expand Down Expand Up @@ -231,7 +234,7 @@ pub(crate) fn integer_indexed_exotic_own_property_keys(
// i. Add ! ToString(𝔽(i)) as the last element of keys.
(0..inner.array_length())
.into_iter()
.map(|index| PropertyKey::Index(index as u32))
.filter_map(|index| NonMaxU32::new(index as u32).map(PropertyKey::Index))
.collect()
};

Expand Down
2 changes: 1 addition & 1 deletion boa_engine/src/object/internal_methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ pub(crate) fn ordinary_own_property_keys(

// 2. For each own property key P of O such that P is an array index, in ascending numeric index order, do
// a. Add P as the last element of keys.
keys.extend(ordered_indexes.into_iter().map(Into::into));
keys.extend(ordered_indexes.into_iter().map(|idx| idx.get().into()));

// 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do
// a. Add P as the last element of keys.
Expand Down
6 changes: 3 additions & 3 deletions boa_engine/src/object/internal_methods/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ pub(crate) fn string_exotic_own_property_keys(
.properties
.index_property_keys()
.copied()
.filter(|idx| (*idx as usize) >= len)
.filter(|idx| (idx.get() as usize) >= len)
.collect();
remaining_indices.sort_unstable();
keys.extend(remaining_indices.into_iter().map(Into::into));
keys.extend(remaining_indices.into_iter().map(|idx| idx.get().into()));

// 7. For each own property key P of O such that Type(P) is String and P is not
// an array index, in ascending chronological order of property creation, do
Expand Down Expand Up @@ -159,7 +159,7 @@ fn string_get_own_property(obj: &JsObject, key: &PropertyKey) -> Option<Property
// 6. If IsIntegralNumber(index) is false, return undefined.
// 7. If index is -0𝔽, return undefined.
let pos = match key {
PropertyKey::Index(index) => *index as usize,
PropertyKey::Index(index) => index.get() as usize,
_ => return None,
};

Expand Down
Loading