#![doc = include_str!("../ABOUT.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
)]
#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
#![allow(
clippy::redundant_pub_crate,
// TODO deny once false positive is fixed (https://github.com/rust-lang/rust-clippy/issues/9626).
clippy::trait_duplication_in_bounds,
// Field names intentionally mirror the encoding type they store.
clippy::struct_field_names
)]
#![cfg_attr(not(feature = "arbitrary"), no_std)]
extern crate alloc;
mod fixed_string;
mod interned_str;
mod raw;
mod sym;
#[cfg(test)]
mod tests;
use alloc::{borrow::Cow, format, string::String, vec::Vec};
use raw::RawInterner;
pub use sym::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JStrRef<'a> {
Utf8(&'a str),
Utf16(&'a [u16]),
}
impl<'a> From<&'a str> for JStrRef<'a> {
fn from(s: &'a str) -> Self {
JStrRef::Utf8(s)
}
}
impl<'a> From<&'a [u16]> for JStrRef<'a> {
fn from(s: &'a [u16]) -> Self {
JStrRef::Utf16(s)
}
}
impl<'a, const N: usize> From<&'a [u16; N]> for JStrRef<'a> {
fn from(s: &'a [u16; N]) -> Self {
JStrRef::Utf16(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct JSInternedStrRef<'a, 'b> {
utf8: Option<&'a str>,
utf16: &'b [u16],
}
impl<'a, 'b> JSInternedStrRef<'a, 'b> {
#[inline]
#[must_use]
pub const fn utf8(&self) -> Option<&'a str> {
self.utf8
}
#[inline]
#[must_use]
pub const fn utf16(&self) -> &'b [u16] {
self.utf16
}
pub fn join<F, G, T>(self, f: F, g: G, prioritize_utf8: bool) -> T
where
F: FnOnce(&'a str) -> T,
G: FnOnce(&'b [u16]) -> T,
{
if prioritize_utf8 && let Some(str) = self.utf8 {
return f(str);
}
g(self.utf16)
}
pub fn join_with_context<C, F, G, T>(self, f: F, g: G, ctx: C, prioritize_utf8: bool) -> T
where
F: FnOnce(&'a str, C) -> T,
G: FnOnce(&'b [u16], C) -> T,
{
if prioritize_utf8 && let Some(str) = self.utf8 {
return f(str, ctx);
}
g(self.utf16, ctx)
}
pub fn into_common<C>(self, prioritize_utf8: bool) -> C
where
C: From<&'a str> + From<&'b [u16]>,
{
self.join(Into::into, Into::into, prioritize_utf8)
}
}
impl core::fmt::Display for JSInternedStrRef<'_, '_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.join_with_context(
core::fmt::Display::fmt,
|js, f| {
char::decode_utf16(js.iter().copied())
.map(|r| match r {
Ok(c) => String::from(c),
Err(e) => format!("\\u{:04X}", e.unpaired_surrogate()),
})
.collect::<String>()
.fmt(f)
},
f,
true,
)
}
}
#[derive(Debug, Default)]
pub struct Interner {
utf8_interner: RawInterner<u8>,
utf16_interner: RawInterner<u16>,
latin1_flags: Vec<bool>,
}
impl Interner {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
utf8_interner: RawInterner::with_capacity(capacity),
utf16_interner: RawInterner::with_capacity(capacity),
latin1_flags: Vec::with_capacity(capacity),
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
COMMON_STRINGS_UTF8.len() + self.utf16_interner.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
COMMON_STRINGS_UTF8.is_empty() && self.utf16_interner.is_empty()
}
pub fn get<'a, T>(&self, string: T) -> Option<Sym>
where
T: Into<JStrRef<'a>>,
{
let string = string.into();
Self::get_common(string).or_else(|| {
let index = match string {
JStrRef::Utf8(s) => self.utf8_interner.get(s.as_bytes()),
JStrRef::Utf16(s) => self.utf16_interner.get(s),
};
unsafe { index.map(|i| Sym::new_unchecked(i + 1 + COMMON_STRINGS_UTF8.len())) }
})
}
pub fn get_or_intern<'a, T>(&mut self, string: T) -> Sym
where
T: Into<JStrRef<'a>>,
{
let string = string.into();
self.get(string).unwrap_or_else(|| {
let (utf8, utf16) = match string {
JStrRef::Utf8(s) => (
Some(Cow::Borrowed(s)),
Cow::Owned(s.encode_utf16().collect()),
),
JStrRef::Utf16(s) => (String::from_utf16(s).ok().map(Cow::Owned), Cow::Borrowed(s)),
};
let index = if let Some(utf8) = utf8 {
self.utf8_interner.intern(utf8.as_bytes())
} else {
self.utf8_interner.intern_static(b"")
};
let utf16_index = self.utf16_interner.intern(&utf16);
assert_eq!(index, utf16_index);
self.latin1_flags.push(utf16.iter().all(|&c| c <= 0xFF));
index
.checked_add(1 + COMMON_STRINGS_UTF8.len())
.and_then(Sym::new)
.expect("Cannot intern new string: integer overflow")
})
}
pub fn get_or_intern_static(&mut self, utf8: &'static str, utf16: &'static [u16]) -> Sym {
self.get(utf8).unwrap_or_else(|| {
let index = self.utf8_interner.intern(utf8.as_bytes());
let utf16_index = self.utf16_interner.intern(utf16);
debug_assert_eq!(index, utf16_index);
self.latin1_flags.push(utf16.iter().all(|&c| c <= 0xFF));
index
.checked_add(1 + COMMON_STRINGS_UTF8.len())
.and_then(Sym::new)
.expect("Cannot intern new string: integer overflow")
})
}
#[must_use]
pub fn resolve(&self, symbol: Sym) -> Option<JSInternedStrRef<'_, '_>> {
let index = symbol.get() - 1;
if let Some(utf8) = COMMON_STRINGS_UTF8.index(index).copied() {
let utf16 = COMMON_STRINGS_UTF16
.get_index(index)
.copied()
.expect("The sizes of both statics must be equal");
return Some(JSInternedStrRef {
utf8: Some(utf8),
utf16,
});
}
let index = index - COMMON_STRINGS_UTF8.len();
if let Some(utf16) = self.utf16_interner.index(index) {
let index = index - (self.utf16_interner.len() - self.utf8_interner.len());
let utf8 = unsafe {
core::str::from_utf8_unchecked(
self.utf8_interner
.index(index)
.expect("both interners must have the same size"),
)
};
return Some(JSInternedStrRef {
utf8: if utf8.is_empty() { None } else { Some(utf8) },
utf16,
});
}
None
}
#[inline]
#[must_use]
pub fn resolve_expect(&self, symbol: Sym) -> JSInternedStrRef<'_, '_> {
self.resolve(symbol).expect("string disappeared")
}
#[inline]
#[must_use]
pub fn is_latin1(&self, symbol: Sym) -> bool {
let index = symbol.get() - 1;
if index < COMMON_STRINGS_UTF8.len() {
return true;
}
let dynamic_index = index - COMMON_STRINGS_UTF8.len();
self.latin1_flags
.get(dynamic_index)
.copied()
.unwrap_or(false)
}
fn get_common(string: JStrRef<'_>) -> Option<Sym> {
match string {
JStrRef::Utf8(s) => COMMON_STRINGS_UTF8.get_index(s).map(|idx| {
unsafe { Sym::new_unchecked(idx + 1) }
}),
JStrRef::Utf16(s) => COMMON_STRINGS_UTF16.get_index_of(&s).map(|idx| {
unsafe { Sym::new_unchecked(idx + 1) }
}),
}
}
}
pub trait ToIndentedString {
fn to_indented_string(&self, interner: &Interner, indentation: usize) -> String;
}
pub trait ToInternedString {
fn to_interned_string(&self, interner: &Interner) -> String;
}
impl<T> ToInternedString for T
where
T: ToIndentedString,
{
fn to_interned_string(&self, interner: &Interner) -> String {
self.to_indented_string(interner, 0)
}
}