use crate::{
builtins::string::is_trimmable_whitespace,
gc::{empty_trace, Finalize, Trace},
};
use rustc_hash::FxHashSet;
use std::{
alloc::{alloc, dealloc, Layout},
borrow::Borrow,
cell::Cell,
hash::{Hash, Hasher},
marker::PhantomData,
ops::Deref,
ptr::{copy_nonoverlapping, NonNull},
};
const CONSTANTS_ARRAY: [&str; 127] = [
"",
",",
":",
"name",
"length",
"arguments",
"prototype",
"constructor",
"null",
"undefined",
"number",
"string",
"symbol",
"bigint",
"object",
"function",
"value",
"get",
"set",
"writable",
"enumerable",
"configurable",
"Object",
"assing",
"create",
"toString",
"valueOf",
"is",
"seal",
"isSealed",
"freeze",
"isFrozen",
"keys",
"values",
"entries",
"Function",
"apply",
"bind",
"call",
"Array",
"from",
"isArray",
"of",
"get [Symbol.species]",
"copyWithin",
"entries",
"every",
"fill",
"filter",
"find",
"findIndex",
"flat",
"flatMap",
"forEach",
"includes",
"indexOf",
"join",
"map",
"reduce",
"reduceRight",
"reverse",
"shift",
"slice",
"some",
"sort",
"unshift",
"push",
"pop",
"String",
"charAt",
"charCodeAt",
"concat",
"endsWith",
"includes",
"indexOf",
"lastIndexOf",
"match",
"matchAll",
"normalize",
"padEnd",
"padStart",
"repeat",
"replace",
"replaceAll",
"search",
"slice",
"split",
"startsWith",
"substring",
"toLowerString",
"toUpperString",
"trim",
"trimEnd",
"trimStart",
"Number",
"Boolean",
"RegExp",
"exec",
"test",
"flags",
"index",
"lastIndex",
"Symbol",
"for",
"keyFor",
"description",
"[Symbol.toPrimitive]",
"",
"Map",
"clear",
"delete",
"get",
"has",
"set",
"size",
"Set",
"Reflect",
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"message",
"Date",
"toJSON",
];
const MAX_CONSTANT_STRING_LENGTH: usize = {
let mut max = 0;
let mut i = 0;
while i < CONSTANTS_ARRAY.len() {
let len = CONSTANTS_ARRAY[i].len();
if len > max {
max = len;
}
i += 1;
}
max
};
thread_local! {
static CONSTANTS: FxHashSet<JsString> = {
let mut constants = FxHashSet::default();
for s in CONSTANTS_ARRAY.iter() {
let s = JsString {
inner: Inner::new(s),
_marker: PhantomData,
};
constants.insert(s);
}
constants
};
}
#[repr(C)]
struct Inner {
len: usize,
refcount: Cell<usize>,
data: [u8; 0],
}
impl Inner {
#[inline]
fn new(s: &str) -> NonNull<Self> {
let inner_layout = Layout::new::<Inner>();
let (layout, offset) = inner_layout
.extend(Layout::array::<u8>(s.len()).unwrap())
.unwrap();
let inner = unsafe {
let inner = alloc(layout) as *mut Inner;
inner.write(Inner {
len: s.len(),
refcount: Cell::new(1),
data: [0; 0],
});
let data = (*inner).data.as_mut_ptr();
debug_assert!(std::ptr::eq(inner.cast::<u8>().add(offset), data));
copy_nonoverlapping(s.as_ptr(), data, s.len());
inner
};
unsafe { NonNull::new_unchecked(inner) }
}
#[inline]
fn concat_array(strings: &[&str]) -> NonNull<Inner> {
let mut total_string_size = 0;
for string in strings {
total_string_size += string.len();
}
let inner_layout = Layout::new::<Inner>();
let (layout, offset) = inner_layout
.extend(Layout::array::<u8>(total_string_size).unwrap())
.unwrap();
let inner = unsafe {
let inner = alloc(layout) as *mut Inner;
inner.write(Inner {
len: total_string_size,
refcount: Cell::new(1),
data: [0; 0],
});
let data = (*inner).data.as_mut_ptr();
debug_assert!(std::ptr::eq(inner.cast::<u8>().add(offset), data));
let mut offset = 0;
for string in strings {
copy_nonoverlapping(string.as_ptr(), data.add(offset), string.len());
offset += string.len();
}
inner
};
unsafe { NonNull::new_unchecked(inner) }
}
#[inline]
unsafe fn dealloc(x: NonNull<Inner>) {
let len = (*x.as_ptr()).len;
let inner_layout = Layout::new::<Inner>();
let (layout, _offset) = inner_layout
.extend(Layout::array::<u8>(len).unwrap())
.unwrap();
dealloc(x.as_ptr() as _, layout);
}
}
pub struct JsString {
inner: NonNull<Inner>,
_marker: PhantomData<std::rc::Rc<str>>,
}
impl Default for JsString {
#[inline]
fn default() -> Self {
Self::new("")
}
}
impl JsString {
#[inline]
pub fn empty() -> Self {
JsString::default()
}
#[inline]
pub fn new<S: AsRef<str>>(s: S) -> Self {
let s = s.as_ref();
if s.len() <= MAX_CONSTANT_STRING_LENGTH {
if let Some(constant) = CONSTANTS.with(|c| c.get(s).cloned()) {
return constant;
}
}
Self {
inner: Inner::new(s),
_marker: PhantomData,
}
}
pub fn concat<T, U>(x: T, y: U) -> JsString
where
T: AsRef<str>,
U: AsRef<str>,
{
let x = x.as_ref();
let y = y.as_ref();
let this = Self {
inner: Inner::concat_array(&[x, y]),
_marker: PhantomData,
};
if this.len() <= MAX_CONSTANT_STRING_LENGTH {
if let Some(constant) = CONSTANTS.with(|c| c.get(&this).cloned()) {
return constant;
}
}
this
}
pub fn concat_array(strings: &[&str]) -> JsString {
let this = Self {
inner: Inner::concat_array(strings),
_marker: PhantomData,
};
if this.len() <= MAX_CONSTANT_STRING_LENGTH {
if let Some(constant) = CONSTANTS.with(|c| c.get(&this).cloned()) {
return constant;
}
}
this
}
#[inline]
fn inner(&self) -> &Inner {
unsafe { self.inner.as_ref() }
}
#[inline]
pub fn as_str(&self) -> &str {
let inner = self.inner();
unsafe {
let slice = std::slice::from_raw_parts(inner.data.as_ptr(), inner.len);
std::str::from_utf8_unchecked(slice)
}
}
#[inline]
pub fn refcount(this: &Self) -> usize {
this.inner().refcount.get()
}
#[inline]
pub fn ptr_eq(x: &Self, y: &Self) -> bool {
x.inner == y.inner
}
pub(crate) fn index_of(&self, search_value: &Self, from_index: usize) -> Option<usize> {
let len = self.encode_utf16().count();
if search_value.is_empty() && from_index <= len {
return Some(from_index);
}
let search_len = search_value.encode_utf16().count();
for i in from_index..=len {
if i as isize > (len as isize - search_len as isize) {
break;
}
let candidate = String::from_utf16_lossy(
&self
.encode_utf16()
.skip(i)
.take(search_len)
.collect::<Vec<u16>>(),
);
if candidate == search_value.as_str() {
return Some(i);
}
}
None
}
pub(crate) fn string_to_number(&self) -> f64 {
let string = self.trim_matches(is_trimmable_whitespace);
match string {
"" => 0.0,
"Infinity" | "+Infinity" => f64::INFINITY,
"-Infinity" => f64::NEG_INFINITY,
_ if matches!(
string
.chars()
.take(4)
.collect::<String>()
.to_ascii_lowercase()
.as_str(),
"inf" | "+inf" | "-inf" | "nan" | "+nan" | "-nan"
) =>
{
f64::NAN
}
_ => fast_float::parse(string).unwrap_or(f64::NAN),
}
}
}
impl Finalize for JsString {}
unsafe impl Trace for JsString {
empty_trace!();
}
impl Clone for JsString {
#[inline]
fn clone(&self) -> Self {
let inner = self.inner();
inner.refcount.set(inner.refcount.get() + 1);
JsString {
inner: self.inner,
_marker: PhantomData,
}
}
}
impl Drop for JsString {
#[inline]
fn drop(&mut self) {
let inner = self.inner();
if inner.refcount.get() == 1 {
unsafe {
Inner::dealloc(self.inner);
}
} else {
inner.refcount.set(inner.refcount.get() - 1);
}
}
}
impl std::fmt::Debug for JsString {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
impl std::fmt::Display for JsString {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
impl From<&str> for JsString {
#[inline]
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<Box<str>> for JsString {
#[inline]
fn from(s: Box<str>) -> Self {
Self::new(s)
}
}
impl From<String> for JsString {
#[inline]
fn from(s: String) -> Self {
Self::new(s)
}
}
impl AsRef<str> for JsString {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for JsString {
#[inline]
fn borrow(&self) -> &str {
self.as_str()
}
}
impl Deref for JsString {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl PartialEq<JsString> for JsString {
#[inline]
fn eq(&self, other: &Self) -> bool {
if Self::ptr_eq(self, other) {
return true;
}
self.as_str() == other.as_str()
}
}
impl Eq for JsString {}
impl Hash for JsString {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl PartialOrd for JsString {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_str().partial_cmp(other.as_str())
}
}
impl Ord for JsString {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_str().cmp(other)
}
}
impl PartialEq<str> for JsString {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<JsString> for str {
#[inline]
fn eq(&self, other: &JsString) -> bool {
self == other.as_str()
}
}
impl PartialEq<&str> for JsString {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<JsString> for &str {
#[inline]
fn eq(&self, other: &JsString) -> bool {
*self == other.as_str()
}
}
#[cfg(test)]
mod tests {
use super::JsString;
use std::mem::size_of;
#[test]
fn empty() {
let _ = JsString::new("");
}
#[test]
fn pointer_size() {
assert_eq!(size_of::<JsString>(), size_of::<*const u8>());
assert_eq!(size_of::<Option<JsString>>(), size_of::<*const u8>());
}
#[test]
fn refcount() {
let x = JsString::new("Hello wrold");
assert_eq!(JsString::refcount(&x), 1);
{
let y = x.clone();
assert_eq!(JsString::refcount(&x), 2);
assert_eq!(JsString::refcount(&y), 2);
{
let z = y.clone();
assert_eq!(JsString::refcount(&x), 3);
assert_eq!(JsString::refcount(&y), 3);
assert_eq!(JsString::refcount(&z), 3);
}
assert_eq!(JsString::refcount(&x), 2);
assert_eq!(JsString::refcount(&y), 2);
}
assert_eq!(JsString::refcount(&x), 1);
}
#[test]
fn ptr_eq() {
let x = JsString::new("Hello");
let y = x.clone();
assert!(JsString::ptr_eq(&x, &y));
let z = JsString::new("Hello");
assert!(!JsString::ptr_eq(&x, &z));
assert!(!JsString::ptr_eq(&y, &z));
}
#[test]
fn as_str() {
let s = "Hello";
let x = JsString::new(s);
assert_eq!(x.as_str(), s);
}
#[test]
fn hash() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let s = "Hello, world!";
let x = JsString::new(s);
assert_eq!(x.as_str(), s);
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
let s_hash = hasher.finish();
let mut hasher = DefaultHasher::new();
x.hash(&mut hasher);
let x_hash = hasher.finish();
assert_eq!(s_hash, x_hash);
}
#[test]
fn concat() {
let x = JsString::new("hello");
let y = ", ";
let z = JsString::new("world");
let w = String::from("!");
let xy = JsString::concat(x, y);
assert_eq!(xy, "hello, ");
assert_eq!(JsString::refcount(&xy), 1);
let xyz = JsString::concat(xy, z);
assert_eq!(xyz, "hello, world");
assert_eq!(JsString::refcount(&xyz), 1);
let xyzw = JsString::concat(xyz, w);
assert_eq!(xyzw, "hello, world!");
assert_eq!(JsString::refcount(&xyzw), 1);
}
}