use crate::builtins::string::is_trimmable_whitespace;
use boa_gc::{unsafe_empty_trace, Finalize, Trace};
use rustc_hash::{FxHashMap, FxHasher};
use std::{
alloc::{alloc, dealloc, handle_alloc_error, Layout},
borrow::Borrow,
cell::Cell,
hash::BuildHasherDefault,
hash::{Hash, Hasher},
marker::PhantomData,
ops::Deref,
ptr::{copy_nonoverlapping, NonNull},
rc::Rc,
};
const CONSTANTS_ARRAY: [&str; 419] = [
"",
",",
":",
"name",
"length",
"arguments",
"prototype",
"constructor",
"return",
"throw",
"global",
"globalThis",
"null",
"undefined",
"number",
"string",
"symbol",
"bigint",
"object",
"function",
"value",
"get",
"set",
"writable",
"enumerable",
"configurable",
"Object",
"assign",
"create",
"toString",
"valueOf",
"is",
"seal",
"isSealed",
"freeze",
"isFrozen",
"isExtensible",
"hasOwnProperty",
"isPrototypeOf",
"setPrototypeOf",
"getPrototypeOf",
"defineProperty",
"defineProperties",
"deleteProperty",
"construct",
"hasOwn",
"ownKeys",
"keys",
"values",
"entries",
"fromEntries",
"Function",
"apply",
"bind",
"call",
"Generator",
"Array",
"at",
"from",
"isArray",
"of",
"copyWithin",
"entries",
"every",
"fill",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"flat",
"flatMap",
"forEach",
"includes",
"indexOf",
"join",
"map",
"next",
"reduce",
"reduceRight",
"reverse",
"shift",
"slice",
"splice",
"some",
"sort",
"unshift",
"push",
"pop",
"String",
"charAt",
"charCodeAt",
"codePointAt",
"concat",
"endsWith",
"fromCharCode",
"fromCodePoint",
"includes",
"indexOf",
"lastIndexOf",
"match",
"matchAll",
"normalize",
"padEnd",
"padStart",
"raw",
"repeat",
"replace",
"replaceAll",
"search",
"slice",
"split",
"startsWith",
"substr",
"substring",
"toLocaleString",
"toLowerCase",
"toUpperCase",
"trim",
"trimEnd",
"trimStart",
"Number",
"Infinity",
"NaN",
"parseInt",
"parseFloat",
"isFinite",
"isNaN",
"parseInt",
"EPSILON",
"MAX_SAFE_INTEGER",
"MIN_SAFE_INTEGER",
"MAX_VALUE",
"MIN_VALUE",
"isSafeInteger",
"isInteger",
"toExponential",
"toFixed",
"toPrecision",
"Boolean",
"BigInt",
"asIntN",
"asUintN",
"RegExp",
"exec",
"test",
"flags",
"index",
"lastIndex",
"hasIndices",
"ignoreCase",
"multiline",
"dotAll",
"unicode",
"sticky",
"source",
"get hasIndices",
"get global",
"get ignoreCase",
"get multiline",
"get dotAll",
"get unicode",
"get sticky",
"get flags",
"get source",
"Symbol",
"for",
"keyFor",
"description",
"asyncIterator",
"hasInstance",
"species",
"Symbol.species",
"unscopables",
"iterator",
"Symbol.iterator",
"Symbol.match",
"[Symbol.match]",
"Symbol.matchAll",
"Symbol.replace",
"[Symbol.replace]",
"Symbol.search",
"[Symbol.search]",
"Symbol.split",
"[Symbol.split]",
"toStringTag",
"toPrimitive",
"get description",
"Map",
"clear",
"delete",
"get",
"has",
"set",
"size",
"Set",
"add",
"Reflect",
"Proxy",
"revocable",
"Error",
"AggregateError",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"ThrowTypeError",
"URIError",
"message",
"Date",
"toJSON",
"getDate",
"getDay",
"getFullYear",
"getHours",
"getMilliseconds",
"getMinutes",
"getMonth",
"getSeconds",
"getTime",
"getYear",
"getUTCDate",
"getUTCDay",
"getUTCFullYear",
"getUTCHours",
"getUTCMinutes",
"getUTCMonth",
"getUTCSeconds",
"setDate",
"setFullYear",
"setHours",
"setMilliseconds",
"setMinutes",
"setMonth",
"setSeconds",
"setYear",
"setTime",
"setUTCDate",
"setUTCFullYear",
"setUTCHours",
"setUTCMinutes",
"setUTCMonth",
"setUTCSeconds",
"toDateString",
"toGMTString",
"toISOString",
"toTimeString",
"toUTCString",
"now",
"UTC",
"JSON",
"parse",
"stringify",
"Array Iterator",
"Set Iterator",
"String Iterator",
"Map Iterator",
"For In Iterator",
"Math",
"LN10",
"LN2",
"LOG10E",
"LOG2E",
"PI",
"SQRT1_2",
"SQRT2",
"abs",
"acos",
"acosh",
"asin",
"asinh",
"atan",
"atanh",
"atan2",
"cbrt",
"ceil",
"clz32",
"cos",
"cosh",
"exp",
"expm1",
"floor",
"fround",
"hypot",
"imul",
"log",
"log1p",
"log10",
"log2",
"max",
"min",
"pow",
"random",
"round",
"sign",
"sin",
"sinh",
"sqrt",
"tan",
"tanh",
"trunc",
"Intl",
"DateTimeFormat",
"TypedArray",
"ArrayBuffer",
"Int8Array",
"Uint8Array",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"BigInt64Array",
"BigUint64Array",
"Float32Array",
"Float64Array",
"buffer",
"byteLength",
"byteOffset",
"isView",
"subarray",
"get byteLength",
"get buffer",
"get byteOffset",
"get size",
"get length",
"DataView",
"getBigInt64",
"getBigUint64",
"getFloat32",
"getFloat64",
"getInt8",
"getInt16",
"getInt32",
"getUint8",
"getUint16",
"getUint32",
"setBigInt64",
"setBigUint64",
"setFloat32",
"setFloat64",
"setInt8",
"setInt16",
"setInt32",
"setUint8",
"setUint16",
"setUint32",
"console",
"assert",
"debug",
"error",
"info",
"trace",
"warn",
"exception",
"count",
"countReset",
"group",
"groupCollapsed",
"groupEnd",
"time",
"timeLog",
"timeEnd",
"dir",
"dirxml",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"_",
"$",
];
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
};
unsafe fn try_alloc(layout: Layout) -> *mut u8 {
let ptr = alloc(layout);
if ptr.is_null() {
handle_alloc_error(layout);
}
ptr
}
thread_local! {
static CONSTANTS: FxHashMap<&'static str, JsString> = {
let mut constants = FxHashMap::with_capacity_and_hasher(
CONSTANTS_ARRAY.len(),
BuildHasherDefault::<FxHasher>::default(),
);
for (idx, &s) in CONSTANTS_ARRAY.iter().enumerate() {
let v = unsafe { JsString::new_static(idx) };
constants.insert(s, v);
}
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::<Self>();
let (layout, offset) = inner_layout
.extend(Layout::array::<u8>(s.len()).expect("failed to create memory layout"))
.expect("failed to extend memory layout");
let inner = unsafe {
let inner = try_alloc(layout).cast::<Self>();
inner.write(Self {
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<Self> {
let mut total_string_size = 0;
for string in strings {
total_string_size += string.len();
}
let inner_layout = Layout::new::<Self>();
let (layout, offset) = inner_layout
.extend(Layout::array::<u8>(total_string_size).expect("failed to create memory layout"))
.expect("failed to extend memory layout");
let inner = unsafe {
let inner = try_alloc(layout).cast::<Self>();
inner.write(Self {
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<Self>) {
let len = (*x.as_ptr()).len;
let inner_layout = Layout::new::<Self>();
let (layout, _offset) = inner_layout
.extend(Layout::array::<u8>(len).expect("failed to create memory layout"))
.expect("failed to extend memory layout");
dealloc(x.as_ptr().cast::<_>(), layout);
}
#[inline]
fn as_str(&self) -> &str {
unsafe {
let slice = std::slice::from_raw_parts(self.data.as_ptr(), self.len);
std::str::from_utf8_unchecked(slice)
}
}
}
#[derive(Finalize)]
pub struct JsString {
inner: TaggedInner,
_marker: PhantomData<Rc<str>>,
}
unsafe impl Trace for JsString {
unsafe_empty_trace!();
}
#[repr(transparent)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
struct TaggedInner(NonNull<Inner>);
impl TaggedInner {
#[inline]
unsafe fn new_heap(inner: NonNull<Inner>) -> Self {
Self(inner)
}
#[inline]
const unsafe fn new_static(idx: usize) -> Self {
Self(NonNull::new_unchecked(((idx << 1) | 1) as *mut _))
}
#[inline]
fn is_static(self) -> bool {
(self.0.as_ptr() as usize) & 1 == 1
}
#[inline]
const unsafe fn get_heap_unchecked(self) -> NonNull<Inner> {
self.0
}
#[inline]
unsafe fn get_static_unchecked(self) -> &'static str {
CONSTANTS_ARRAY.get_unchecked((self.0.as_ptr() as usize) >> 1)
}
}
impl Default for JsString {
#[inline]
fn default() -> Self {
unsafe { Self::new_static(0) }
}
}
enum InnerKind<'a> {
Heap(&'a Inner),
Static(&'static str),
}
impl JsString {
#[inline]
unsafe fn new_static(idx: usize) -> Self {
Self {
inner: TaggedInner::new_static(idx),
_marker: PhantomData,
}
}
#[inline]
pub fn empty() -> Self {
Self::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: unsafe { TaggedInner::new_heap(Inner::new(s)) },
_marker: PhantomData,
}
}
pub fn concat<T, U>(x: T, y: U) -> Self
where
T: AsRef<str>,
U: AsRef<str>,
{
let x = x.as_ref();
let y = y.as_ref();
let inner = Inner::concat_array(&[x, y]);
let s = unsafe { inner.as_ref() }.as_str();
if s.len() <= MAX_CONSTANT_STRING_LENGTH {
if let Some(constant) = CONSTANTS.with(|c| c.get(s).cloned()) {
unsafe { Inner::dealloc(inner) };
return constant;
}
}
Self {
inner: unsafe { TaggedInner::new_heap(inner) },
_marker: PhantomData,
}
}
pub fn concat_array(strings: &[&str]) -> Self {
let inner = Inner::concat_array(strings);
let s = unsafe { inner.as_ref() }.as_str();
if s.len() <= MAX_CONSTANT_STRING_LENGTH {
if let Some(constant) = CONSTANTS.with(|c| c.get(s).cloned()) {
unsafe { Inner::dealloc(inner) };
return constant;
}
}
Self {
inner: unsafe { TaggedInner::new_heap(inner) },
_marker: PhantomData,
}
}
#[inline]
fn inner(&self) -> InnerKind<'_> {
if self.inner.is_static() {
InnerKind::Static(unsafe { self.inner.get_static_unchecked() })
} else {
InnerKind::Heap(unsafe { self.inner.get_heap_unchecked().as_ref() })
}
}
#[inline]
pub fn as_str(&self) -> &str {
match self.inner() {
InnerKind::Heap(inner) => inner.as_str(),
InnerKind::Static(inner) => inner,
}
}
#[inline]
pub fn refcount(this: &Self) -> Option<usize> {
match this.inner() {
InnerKind::Heap(inner) => Some(inner.refcount.get()),
InnerKind::Static(_inner) => None,
}
}
#[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 {
"" => return 0.0,
"-Infinity" => return f64::NEG_INFINITY,
"Infinity" | "+Infinity" => return f64::INFINITY,
_ => {}
}
let mut s = string.bytes();
let base = match (s.next(), s.next()) {
(Some(b'0'), Some(b'b' | b'B')) => Some(2),
(Some(b'0'), Some(b'o' | b'O')) => Some(8),
(Some(b'0'), Some(b'x' | b'X')) => Some(16),
_ => None,
};
if let Some(base) = base {
let string = &string[2..];
if string.is_empty() {
return f64::NAN;
}
if let Ok(value) = u32::from_str_radix(string, base) {
return f64::from(value);
}
let mut value = 0.0;
for c in s {
if let Some(digit) = char::from(c).to_digit(base) {
value = value * f64::from(base) + f64::from(digit);
} else {
return f64::NAN;
}
}
return value;
}
match string {
"inf" | "+inf" | "-inf" => f64::NAN,
string => fast_float::parse(string).unwrap_or(f64::NAN),
}
}
}
impl Clone for JsString {
#[inline]
fn clone(&self) -> Self {
if let InnerKind::Heap(inner) = self.inner() {
inner.refcount.set(inner.refcount.get() + 1);
}
Self {
inner: self.inner,
_marker: PhantomData,
}
}
}
impl Drop for JsString {
#[inline]
fn drop(&mut self) {
if let InnerKind::Heap(inner) = self.inner() {
if inner.refcount.get() == 1 {
unsafe {
Inner::dealloc(self.inner.get_heap_unchecked());
}
} 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<Self> 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 _empty = 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), Some(1));
{
let y = x.clone();
assert_eq!(JsString::refcount(&x), Some(2));
assert_eq!(JsString::refcount(&y), Some(2));
{
let z = y.clone();
assert_eq!(JsString::refcount(&x), Some(3));
assert_eq!(JsString::refcount(&y), Some(3));
assert_eq!(JsString::refcount(&z), Some(3));
}
assert_eq!(JsString::refcount(&x), Some(2));
assert_eq!(JsString::refcount(&y), Some(2));
}
assert_eq!(JsString::refcount(&x), Some(1));
}
#[test]
fn static_refcount() {
let x = JsString::new("");
assert_eq!(JsString::refcount(&x), None);
{
let y = x.clone();
assert_eq!(JsString::refcount(&x), None);
assert_eq!(JsString::refcount(&y), None);
};
assert_eq!(JsString::refcount(&x), None);
}
#[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 static_ptr_eq() {
let x = JsString::new("");
let y = x.clone();
assert!(JsString::ptr_eq(&x, &y));
let z = JsString::new("");
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), Some(1));
let xyz = JsString::concat(xy, z);
assert_eq!(xyz, "hello, world");
assert_eq!(JsString::refcount(&xyz), Some(1));
let xyzw = JsString::concat(xyz, w);
assert_eq!(xyzw, "hello, world!");
assert_eq!(JsString::refcount(&xyzw), Some(1));
}
}