use crate::platform::CompiledRegex;
use crate::prelude::*;
pub fn number_to_string(n: f64) -> String {
if n.is_nan() {
return "NaN".to_string();
}
if n.is_infinite() {
return if n > 0.0 {
"Infinity".to_string()
} else {
"-Infinity".to_string()
};
}
if n == 0.0 {
return "0".to_string();
}
let abs_n = n.abs();
if math::trunc(n) == n && abs_n < 1e21 {
return format!("{:.0}", n);
}
if !(1e-6..1e21).contains(&abs_n) {
format_exponential(n)
} else {
format_decimal(n)
}
}
fn format_exponential(n: f64) -> String {
let abs_n = n.abs();
let exponent = math::floor(math::log10(abs_n)) as i32;
let mantissa = n / math::powi(10_f64, exponent);
let mantissa_str = if math::trunc(mantissa) == mantissa {
format!("{:.0}", mantissa)
} else {
let s = format!("{}", mantissa);
s.trim_end_matches('0').to_string()
};
if exponent >= 0 {
format!("{}e+{}", mantissa_str, exponent)
} else {
format!("{}e{}", mantissa_str, exponent)
}
}
fn format_decimal(n: f64) -> String {
let s = format!("{}", n);
if s.contains('.') {
let trimmed = s.trim_end_matches('0');
if trimmed.ends_with('.') {
format!("{}0", trimmed)
} else {
trimmed.to_string()
}
} else {
s
}
}
pub fn string_to_number(s: &str) -> f64 {
let trimmed = trim_js_whitespace(s);
if trimmed.is_empty() {
return 0.0;
}
if trimmed == "Infinity" || trimmed == "+Infinity" {
return f64::INFINITY;
}
if trimmed == "-Infinity" {
return f64::NEG_INFINITY;
}
if trimmed.len() >= 2 {
let bytes = trimmed.as_bytes();
if bytes.first() == Some(&b'0') {
match bytes.get(1) {
Some(b'x' | b'X') => {
let hex_part = trimmed.get(2..).unwrap_or("");
if hex_part.is_empty() {
return f64::NAN;
}
return match u64::from_str_radix(hex_part, 16) {
Ok(n) => n as f64,
Err(_) => f64::NAN,
};
}
Some(b'o' | b'O') => {
let oct_part = trimmed.get(2..).unwrap_or("");
if oct_part.is_empty() {
return f64::NAN;
}
return match u64::from_str_radix(oct_part, 8) {
Ok(n) => n as f64,
Err(_) => f64::NAN,
};
}
Some(b'b' | b'B') => {
let bin_part = trimmed.get(2..).unwrap_or("");
if bin_part.is_empty() {
return f64::NAN;
}
return match u64::from_str_radix(bin_part, 2) {
Ok(n) => n as f64,
Err(_) => f64::NAN,
};
}
_ => {}
}
}
}
let lower = trimmed.to_lowercase();
if lower.contains("infinity") || lower.contains("inf") {
return f64::NAN;
}
if lower == "nan" && trimmed != "NaN" {
return f64::NAN;
}
trimmed.parse::<f64>().unwrap_or(f64::NAN)
}
fn trim_js_whitespace(s: &str) -> &str {
fn is_js_whitespace(c: char) -> bool {
matches!(
c,
' ' | '\t'
| '\n'
| '\r'
| '\x0B'
| '\x0C'
| '\u{00A0}'
| '\u{FEFF}'
| '\u{2028}'
| '\u{2029}'
| '\u{1680}'
| '\u{2000}'
| '\u{2001}'
| '\u{2002}'
| '\u{2003}'
| '\u{2004}'
| '\u{2005}'
| '\u{2006}'
| '\u{2007}'
| '\u{2008}'
| '\u{2009}'
| '\u{200A}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
)
}
s.trim_matches(is_js_whitespace)
}
use crate::ast::{BlockStatement, FunctionParam};
use crate::error::JsError;
use crate::gc::{Gc, GcPtr, Guard, Heap, Reset, Traceable};
pub trait CheapClone: Clone {
fn cheap_clone(&self) -> Self {
self.clone()
}
}
impl<T: ?Sized> CheapClone for Rc<T> {}
impl<T: CheapClone> CheapClone for Option<T> {}
#[derive(Clone, Default)]
pub enum JsValue {
#[default]
Undefined,
Null,
Boolean(bool),
Number(f64),
String(JsString),
Symbol(Box<JsSymbol>),
Object(JsObjectRef),
}
pub struct Guarded {
pub value: JsValue,
pub guard: Option<Guard<JsObject>>,
}
impl fmt::Debug for Guarded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Guarded")
.field("value", &self.value)
.field("guard", &self.guard.as_ref().map(|_| "<guard>"))
.finish()
}
}
impl Guarded {
pub fn with_guard(value: JsValue, guard: Guard<JsObject>) -> Self {
if matches!(value, JsValue::Object(_)) {
Self {
value,
guard: Some(guard),
}
} else {
drop(guard);
Self { value, guard: None }
}
}
pub fn unguarded(value: JsValue) -> Self {
Self { value, guard: None }
}
pub fn from_value(value: JsValue, heap: &Heap<JsObject>) -> Self {
if let JsValue::Object(obj) = &value {
let guard = heap.create_guard();
guard.guard(obj.cheap_clone());
Self {
value,
guard: Some(guard),
}
} else {
Self { value, guard: None }
}
}
pub fn with_value(self, value: JsValue) -> Self {
Self {
value,
guard: self.guard,
}
}
}
impl JsValue {
pub fn is_null_or_undefined(&self) -> bool {
matches!(self, JsValue::Null | JsValue::Undefined)
}
pub fn is_callable(&self) -> bool {
match self {
JsValue::Object(obj) => {
matches!(obj.borrow().exotic, ExoticObject::Function(_))
}
_ => false,
}
}
pub fn is_string(&self) -> bool {
matches!(self, JsValue::String(_))
}
pub fn is_undefined(&self) -> bool {
matches!(self, JsValue::Undefined)
}
pub fn is_null(&self) -> bool {
matches!(self, JsValue::Null)
}
pub fn is_nullish(&self) -> bool {
matches!(self, JsValue::Null | JsValue::Undefined)
}
pub fn is_boolean(&self) -> bool {
matches!(self, JsValue::Boolean(_))
}
pub fn is_number(&self) -> bool {
matches!(self, JsValue::Number(_))
}
pub fn is_object(&self) -> bool {
matches!(self, JsValue::Object(_))
}
pub fn is_symbol(&self) -> bool {
matches!(self, JsValue::Symbol(_))
}
pub fn as_bool(&self) -> Option<bool> {
match self {
JsValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_number(&self) -> Option<f64> {
match self {
JsValue::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
JsValue::String(s) => Some(s.as_str()),
_ => None,
}
}
pub fn as_js_string(&self) -> Option<&JsString> {
match self {
JsValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_object(&self) -> Option<&Gc<JsObject>> {
match self {
JsValue::Object(obj) => Some(obj),
_ => None,
}
}
pub fn as_symbol(&self) -> Option<&JsSymbol> {
match self {
JsValue::Symbol(s) => Some(s),
_ => None,
}
}
pub fn guard_by(&self, guard: &Guard<JsObject>) {
if let JsValue::Object(obj) = self {
guard.guard(obj.cheap_clone());
}
}
pub fn to_boolean(&self) -> bool {
match self {
JsValue::Undefined | JsValue::Null => false,
JsValue::Boolean(b) => *b,
JsValue::Number(n) => *n != 0.0 && !n.is_nan(),
JsValue::String(s) => !s.is_empty(),
JsValue::Symbol(_) => true, JsValue::Object(_) => true,
}
}
pub fn to_number(&self) -> f64 {
match self {
JsValue::Undefined => f64::NAN,
JsValue::Null => 0.0,
JsValue::Boolean(true) => 1.0,
JsValue::Boolean(false) => 0.0,
JsValue::Number(n) => *n,
JsValue::String(s) => string_to_number(s.as_str()),
JsValue::Symbol(_) => f64::NAN, JsValue::Object(obj) => {
let borrowed = obj.borrow();
match &borrowed.exotic {
ExoticObject::Number(n) => *n,
ExoticObject::Boolean(b) => {
if *b {
1.0
} else {
0.0
}
}
ExoticObject::StringObj(s) => string_to_number(s.as_str()),
_ => f64::NAN, }
}
}
}
pub fn to_js_string(&self) -> JsString {
match self {
JsValue::Undefined => JsString::from("undefined"),
JsValue::Null => JsString::from("null"),
JsValue::Boolean(true) => JsString::from("true"),
JsValue::Boolean(false) => JsString::from("false"),
JsValue::Number(n) => JsString::from(number_to_string(*n)),
JsValue::String(s) => s.clone(),
JsValue::Symbol(s) => {
match &s.description {
Some(desc) => JsString::from(format!("Symbol({})", desc.as_str())),
None => JsString::from("Symbol()"),
}
}
JsValue::Object(obj) => {
let borrowed = obj.borrow();
match &borrowed.exotic {
ExoticObject::Number(n) => {
JsValue::Number(*n).to_js_string()
}
ExoticObject::StringObj(s) => {
s.clone()
}
ExoticObject::Boolean(b) => {
if *b {
JsString::from("true")
} else {
JsString::from("false")
}
}
ExoticObject::Array { elements } => {
let strings: Vec<String> = elements
.iter()
.map(|v| {
match v {
JsValue::Null | JsValue::Undefined => String::new(),
_ => v.to_js_string().to_string(),
}
})
.collect();
JsString::from(strings.join(","))
}
_ => JsString::from("[object Object]"),
}
}
}
}
pub fn strict_equals(&self, other: &JsValue) -> bool {
match (self, other) {
(JsValue::Undefined, JsValue::Undefined) => true,
(JsValue::Null, JsValue::Null) => true,
(JsValue::Boolean(a), JsValue::Boolean(b)) => a == b,
(JsValue::Number(a), JsValue::Number(b)) => {
if a.is_nan() || b.is_nan() {
false
} else {
a == b
}
}
(JsValue::String(a), JsValue::String(b)) => a == b,
(JsValue::Symbol(a), JsValue::Symbol(b)) => a == b, (JsValue::Object(a), JsValue::Object(b)) => Gc::ptr_eq(a, b),
_ => false,
}
}
}
fn format_value_for_debug(value: &JsValue) -> String {
match value {
JsValue::Undefined => "undefined".to_string(),
JsValue::Null => "null".to_string(),
JsValue::Boolean(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
JsValue::Number(n) => number_to_string(*n),
JsValue::String(s) => s.to_string(),
JsValue::Symbol(_) => "Symbol()".to_string(),
JsValue::Object(_) => "[object Object]".to_string(),
}
}
impl fmt::Display for JsValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JsValue::Undefined => write!(f, "undefined"),
JsValue::Null => write!(f, "null"),
JsValue::Boolean(b) => write!(f, "{}", b),
JsValue::Number(n) => write!(f, "{}", number_to_string(*n)),
JsValue::String(s) => write!(f, "{}", s.as_str()),
JsValue::Symbol(s) => match &s.description {
Some(desc) => write!(f, "Symbol({})", desc.as_str()),
None => write!(f, "Symbol()"),
},
JsValue::Object(_) => write!(f, "[object Object]"),
}
}
}
impl fmt::Debug for JsValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JsValue::Undefined => write!(f, "undefined"),
JsValue::Null => write!(f, "null"),
JsValue::Boolean(b) => write!(f, "{}", b),
JsValue::Number(n) => write!(f, "{}", n),
JsValue::String(s) => write!(f, "\"{}\"", s.as_ref()),
JsValue::Symbol(s) => match &s.description {
Some(desc) => write!(f, "Symbol({})", desc.as_str()),
None => write!(f, "Symbol()"),
},
JsValue::Object(obj) => {
let obj = obj.borrow();
match &obj.exotic {
ExoticObject::Ordinary => {
let name_key = PropertyKey::String(JsString::from("name"));
let message_key = PropertyKey::String(JsString::from("message"));
if let (Some(name_prop), Some(message_prop)) = (
obj.properties.get(&name_key),
obj.properties.get(&message_key),
) {
let name = format_value_for_debug(&name_prop.value);
let msg = format_value_for_debug(&message_prop.value);
write!(f, "{}: {}", name, msg)
} else {
write!(f, "{{...}}")
}
}
ExoticObject::Array { .. } => write!(f, "[...]"),
ExoticObject::Function(func) => {
let name = func.name().unwrap_or("anonymous");
write!(f, "[Function: {}]", name)
}
ExoticObject::Map { entries } => write!(f, "Map({})", entries.len()),
ExoticObject::Set { entries } => write!(f, "Set({})", entries.len()),
ExoticObject::Date { timestamp } => write!(f, "Date({})", timestamp),
ExoticObject::RegExp { pattern, flags, .. } => {
write!(f, "/{}/{}", pattern, flags)
}
ExoticObject::Generator(_) => write!(f, "[object Generator]"),
ExoticObject::BytecodeGenerator(_) => write!(f, "[object Generator]"),
ExoticObject::Promise(state) => {
let status = match state.borrow().status {
PromiseStatus::Pending => "pending",
PromiseStatus::Fulfilled => "fulfilled",
PromiseStatus::Rejected => "rejected",
};
write!(f, "Promise {{{}}}", status)
}
ExoticObject::Environment(env_data) => {
write!(f, "[Environment {} bindings]", env_data.bindings.len())
}
ExoticObject::Enum(data) => {
write!(f, "enum {}", data.name)
}
ExoticObject::Proxy(proxy_data) => {
if proxy_data.revoked {
write!(f, "[Proxy (revoked)]")
} else {
write!(f, "[Proxy]")
}
}
ExoticObject::Boolean(b) => write!(f, "[Boolean: {}]", b),
ExoticObject::Number(n) => write!(f, "[Number: {}]", n),
ExoticObject::StringObj(s) => write!(f, "[String: \"{}\"]", s),
ExoticObject::Symbol(sym) => match &sym.description {
Some(desc) => write!(f, "[Symbol: Symbol({})]", desc.as_str()),
None => write!(f, "[Symbol: Symbol()]"),
},
ExoticObject::RawJSON(raw) => write!(f, "[RawJSON: {}]", raw),
ExoticObject::PendingOrder { id, .. } => write!(f, "[PendingOrder: {}]", id),
}
}
}
}
}
impl PartialEq for JsValue {
fn eq(&self, other: &Self) -> bool {
self.strict_equals(other)
}
}
impl From<bool> for JsValue {
fn from(b: bool) -> Self {
JsValue::Boolean(b)
}
}
impl From<f64> for JsValue {
fn from(n: f64) -> Self {
JsValue::Number(n)
}
}
impl From<i32> for JsValue {
fn from(n: i32) -> Self {
JsValue::Number(n as f64)
}
}
impl From<i64> for JsValue {
fn from(n: i64) -> Self {
JsValue::Number(n as f64)
}
}
impl From<u32> for JsValue {
fn from(n: u32) -> Self {
JsValue::Number(n as f64)
}
}
impl From<u64> for JsValue {
fn from(n: u64) -> Self {
JsValue::Number(n as f64)
}
}
impl From<usize> for JsValue {
fn from(n: usize) -> Self {
JsValue::Number(n as f64)
}
}
impl From<()> for JsValue {
fn from(_: ()) -> Self {
JsValue::Undefined
}
}
impl From<&str> for JsValue {
fn from(s: &str) -> Self {
JsValue::String(JsString::from(s))
}
}
impl From<String> for JsValue {
fn from(s: String) -> Self {
JsValue::String(JsString::from(s))
}
}
impl From<JsString> for JsValue {
fn from(s: JsString) -> Self {
JsValue::String(s)
}
}
impl JsValue {
pub fn type_name(&self) -> &'static str {
match self {
JsValue::Undefined => "undefined",
JsValue::Null => "null",
JsValue::Boolean(_) => "boolean",
JsValue::Number(_) => "number",
JsValue::String(_) => "string",
JsValue::Symbol(_) => "symbol",
JsValue::Object(_) => "object",
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct JsString(Rc<str>);
#[derive(Clone)]
pub struct VarKey(pub JsString);
impl Hash for VarKey {
fn hash<H: Hasher>(&self, state: &mut H) {
let ptr = Rc::as_ptr(&self.0.0) as *const () as usize;
ptr.hash(state);
}
}
impl PartialEq for VarKey {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0.0, &other.0.0)
}
}
impl Eq for VarKey {}
impl fmt::Debug for VarKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "VarKey({:?})", self.0)
}
}
impl fmt::Display for VarKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<JsString> for VarKey {
fn from(s: JsString) -> Self {
VarKey(s)
}
}
pub type ClassBrandId = u32;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PrivateFieldKey {
pub class_brand: ClassBrandId,
pub field_name: JsString,
}
impl PrivateFieldKey {
pub fn new(class_brand: ClassBrandId, field_name: JsString) -> Self {
Self {
class_brand,
field_name,
}
}
}
impl CheapClone for JsString {}
impl JsString {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn parse<F: core::str::FromStr>(&self) -> Result<F, F::Err> {
self.0.parse()
}
}
impl AsRef<str> for JsString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl core::borrow::Borrow<str> for JsString {
fn borrow(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for JsString {
fn eq(&self, other: &str) -> bool {
self.0.as_ref() == other
}
}
impl PartialEq<&str> for JsString {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}
impl From<&str> for JsString {
fn from(s: &str) -> Self {
JsString(s.into())
}
}
impl From<String> for JsString {
fn from(s: String) -> Self {
JsString(s.into())
}
}
impl fmt::Debug for JsString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
impl fmt::Display for JsString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl core::ops::Add<&str> for JsString {
type Output = JsString;
fn add(self, other: &str) -> JsString {
let mut s = String::from(&*self.0);
s.push_str(other);
JsString::from(s)
}
}
impl core::ops::Add<&JsString> for JsString {
type Output = JsString;
fn add(self, other: &JsString) -> JsString {
let mut s = String::from(&*self.0);
s.push_str(&other.0);
JsString::from(s)
}
}
#[derive(Clone, Debug)]
pub struct JsSymbol {
id: u64,
pub description: Option<JsString>,
}
impl JsSymbol {
pub fn new(id: u64, description: Option<JsString>) -> Self {
Self { id, description }
}
pub fn id(&self) -> u64 {
self.id
}
}
impl PartialEq for JsSymbol {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for JsSymbol {}
impl Hash for JsSymbol {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
pub type JsObjectRef = Gc<JsObject>;
impl<T: Default + Reset + Traceable> CheapClone for Gc<T> {}
impl Reset for JsObject {
fn reset(&mut self) {
self.prototype = None;
self.extensible = true;
self.frozen = false;
self.sealed = false;
self.null_prototype = false;
self.properties.clear();
self.exotic = ExoticObject::Ordinary;
self.private_fields = None;
}
}
impl Traceable for JsObject {
fn trace<F: FnMut(GcPtr<Self>)>(&self, mut visitor: F) {
if let Some(proto) = &self.prototype {
visitor(proto.copy_ref());
}
for prop in self.properties.values() {
if let JsValue::Object(obj) = &prop.value {
visitor(obj.copy_ref());
}
if let Some(accessor) = &prop.accessor {
if let Some(getter) = &accessor.getter {
visitor(getter.copy_ref());
}
if let Some(setter) = &accessor.setter {
visitor(setter.copy_ref());
}
}
}
match &self.exotic {
ExoticObject::Function(func) => {
match func {
JsFunction::Bound(bound) => {
visitor(bound.target.copy_ref());
if let JsValue::Object(obj) = &bound.this_arg {
visitor(obj.copy_ref());
}
for arg in &bound.bound_args {
if let JsValue::Object(obj) = arg {
visitor(obj.copy_ref());
}
}
}
JsFunction::PromiseResolve(promise) | JsFunction::PromiseReject(promise) => {
visitor(promise.copy_ref());
}
JsFunction::PromiseAllFulfill { state, .. } => {
visitor(state.result_promise.copy_ref());
for result in state.results.borrow().iter() {
if let JsValue::Object(obj) = result {
visitor(obj.copy_ref());
}
}
}
JsFunction::PromiseAllReject(state) => {
visitor(state.result_promise.copy_ref());
for result in state.results.borrow().iter() {
if let JsValue::Object(obj) = result {
visitor(obj.copy_ref());
}
}
}
JsFunction::PromiseRaceSettle { state, .. } => {
visitor(state.result_promise.copy_ref());
}
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => {
visitor(bc.closure.copy_ref());
if let Some(this) = &bc.captured_this
&& let JsValue::Object(obj) = this.as_ref()
{
visitor(obj.copy_ref());
}
}
JsFunction::ModuleExportGetter { module_env, .. } => {
visitor(module_env.copy_ref());
}
JsFunction::ModuleReExportGetter { source_module, .. } => {
visitor(source_module.copy_ref());
}
JsFunction::ProxyRevoke(proxy) => {
visitor(proxy.copy_ref());
}
JsFunction::Native(_)
| JsFunction::AccessorGetter
| JsFunction::AccessorSetter => {}
}
}
ExoticObject::Map { entries } => {
for (k, v) in entries {
if let JsValue::Object(obj) = &k.0 {
visitor(obj.copy_ref());
}
if let JsValue::Object(obj) = v {
visitor(obj.copy_ref());
}
}
}
ExoticObject::Set { entries } => {
for entry in entries {
if let JsValue::Object(obj) = &entry.0 {
visitor(obj.copy_ref());
}
}
}
ExoticObject::Promise(state) => {
let state = state.borrow();
if let Some(JsValue::Object(obj)) = &state.result {
visitor(obj.copy_ref());
}
for handler in &state.handlers {
if let Some(JsValue::Object(obj)) = &handler.on_fulfilled {
visitor(obj.copy_ref());
}
if let Some(JsValue::Object(obj)) = &handler.on_rejected {
visitor(obj.copy_ref());
}
visitor(handler.result_promise.copy_ref());
}
}
ExoticObject::Generator(state) => {
let state = state.borrow();
visitor(state.closure.copy_ref());
for arg in &state.args {
if let JsValue::Object(obj) = arg {
visitor(obj.copy_ref());
}
}
if let JsValue::Object(obj) = &state.sent_value {
visitor(obj.copy_ref());
}
}
ExoticObject::BytecodeGenerator(state) => {
let state = state.borrow();
visitor(state.closure.copy_ref());
for arg in &state.args {
if let JsValue::Object(obj) = arg {
visitor(obj.copy_ref());
}
}
if let JsValue::Object(obj) = &state.this_value {
visitor(obj.copy_ref());
}
for reg in &state.saved_registers {
if let JsValue::Object(obj) = reg {
visitor(obj.copy_ref());
}
}
if let JsValue::Object(obj) = &state.sent_value {
visitor(obj.copy_ref());
}
if let Some(env) = &state.func_env {
visitor(env.copy_ref());
}
if let Some(env) = &state.current_env {
visitor(env.copy_ref());
}
if let Some((iter_obj, next_method)) = &state.delegated_iterator {
visitor(iter_obj.copy_ref());
if let JsValue::Object(obj) = next_method {
visitor(obj.copy_ref());
}
}
if let Some(JsValue::Object(obj)) = &state.throw_value {
visitor(obj.copy_ref());
}
}
ExoticObject::Environment(env_data) => {
for binding in env_data.bindings.values() {
if let JsValue::Object(obj) = &binding.value {
visitor(obj.copy_ref());
}
}
if let Some(outer) = &env_data.outer {
visitor(outer.copy_ref());
}
}
ExoticObject::Array { elements } => {
for elem in elements {
if let JsValue::Object(obj) = elem {
visitor(obj.copy_ref());
}
}
}
ExoticObject::Ordinary
| ExoticObject::Date { .. }
| ExoticObject::RegExp { .. }
| ExoticObject::Enum(_)
| ExoticObject::Boolean(_)
| ExoticObject::Number(_)
| ExoticObject::StringObj(_)
| ExoticObject::Symbol(_)
| ExoticObject::RawJSON(_)
| ExoticObject::PendingOrder { .. } => {
}
ExoticObject::Proxy(proxy_data) => {
visitor(proxy_data.target.copy_ref());
visitor(proxy_data.handler.copy_ref());
}
}
if let Some(private_fields) = &self.private_fields {
for value in private_fields.values() {
if let JsValue::Object(obj) = value {
visitor(obj.copy_ref());
}
}
}
}
}
#[derive(Debug)]
pub struct JsObject {
pub prototype: Option<JsObjectRef>,
pub extensible: bool,
pub frozen: bool,
pub sealed: bool,
pub null_prototype: bool,
pub properties: PropertyStorage,
pub exotic: ExoticObject,
pub private_fields: Option<FxHashMap<PrivateFieldKey, JsValue>>,
}
impl JsObject {
pub fn new() -> Self {
Self {
prototype: None,
extensible: true,
frozen: false,
sealed: false,
null_prototype: false,
properties: PropertyStorage::new(),
exotic: ExoticObject::Ordinary,
private_fields: None,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
prototype: None,
extensible: true,
frozen: false,
sealed: false,
null_prototype: false,
properties: PropertyStorage::with_capacity(capacity),
exotic: ExoticObject::Ordinary,
private_fields: None,
}
}
pub fn with_prototype(prototype: JsObjectRef) -> Self {
Self {
prototype: Some(prototype),
extensible: true,
frozen: false,
sealed: false,
null_prototype: false,
properties: PropertyStorage::new(),
exotic: ExoticObject::Ordinary,
private_fields: None,
}
}
pub fn get_private_field(&self, key: &PrivateFieldKey) -> Option<&JsValue> {
self.private_fields.as_ref().and_then(|pf| pf.get(key))
}
pub fn set_private_field(&mut self, key: PrivateFieldKey, value: JsValue) {
self.private_fields
.get_or_insert_with(FxHashMap::default)
.insert(key, value);
}
pub fn has_private_field(&self, key: &PrivateFieldKey) -> bool {
self.private_fields
.as_ref()
.is_some_and(|pf| pf.contains_key(key))
}
pub fn is_callable(&self) -> bool {
match &self.exotic {
ExoticObject::Function(_) => true,
ExoticObject::Proxy(data) => {
!data.revoked && data.target.borrow().is_callable()
}
_ => false,
}
}
pub fn get_own_property(&self, key: &PropertyKey) -> Option<&Property> {
self.properties.get(key)
}
pub fn get_property(&self, key: &PropertyKey) -> Option<JsValue> {
if let ExoticObject::Array { ref elements } = self.exotic {
match key {
PropertyKey::Index(idx) => {
return elements.get(*idx as usize).cloned();
}
PropertyKey::String(s) if s.as_str() == "length" => {
return Some(JsValue::Number(elements.len() as f64));
}
_ => {}
}
}
if let ExoticObject::Function(ref func) = self.exotic
&& let PropertyKey::String(s) = key
{
match s.as_str() {
"name" => {
if let Some(prop) = self.properties.get(key) {
return Some(prop.value.clone());
}
let name = match func {
JsFunction::Native(f) => Some(f.name.clone()),
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => bc
.chunk
.function_info
.as_ref()
.and_then(|info| info.name.clone()),
JsFunction::Bound(b) => {
if let ExoticObject::Function(target_func) = &b.target.borrow().exotic {
match target_func {
JsFunction::Native(f) => {
Some(JsString::from(format!("bound {}", f.name)))
}
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => {
bc.chunk.function_info.as_ref().and_then(|info| {
info.name
.as_ref()
.map(|n| JsString::from(format!("bound {}", n)))
})
}
_ => Some(JsString::from("bound ")),
}
} else {
Some(JsString::from("bound "))
}
}
_ => None,
};
return Some(match name {
Some(n) => JsValue::String(n),
None => JsValue::String(JsString::from("")),
});
}
"length" => {
let arity = match func {
JsFunction::Native(f) => f.arity,
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => bc
.chunk
.function_info
.as_ref()
.map(|info| info.param_count)
.unwrap_or(0),
JsFunction::Bound(b) => {
if let ExoticObject::Function(target_func) = &b.target.borrow().exotic {
let target_length = match target_func {
JsFunction::Native(f) => f.arity,
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => bc
.chunk
.function_info
.as_ref()
.map(|info| info.param_count)
.unwrap_or(0),
_ => 0,
};
target_length.saturating_sub(b.bound_args.len())
} else {
0
}
}
_ => 0,
};
return Some(JsValue::Number(arity as f64));
}
_ => {}
}
}
if let ExoticObject::Enum(ref data) = self.exotic {
match key {
PropertyKey::String(s) => {
if let Some(val) = data.get_by_name(s.as_str()) {
return Some(val);
}
if let Ok(n) = s.as_str().parse::<f64>()
&& let Some(name) = data.get_by_value(n)
{
return Some(JsValue::String(name));
}
}
PropertyKey::Index(idx) => {
if let Some(name) = data.get_by_value(*idx as f64) {
return Some(JsValue::String(name));
}
}
PropertyKey::Symbol(_) => {}
}
}
if let Some(prop) = self.properties.get(key) {
return Some(prop.value.clone());
}
if let Some(ref proto) = self.prototype {
return proto.borrow().get_property(key);
}
None
}
pub fn get_property_descriptor(&self, key: &PropertyKey) -> Option<(Property, bool)> {
if let ExoticObject::Array { ref elements } = self.exotic {
match key {
PropertyKey::Index(idx) => {
if let Some(val) = elements.get(*idx as usize) {
return Some((Property::data(val.clone()), false));
}
}
PropertyKey::String(s) if s.as_str() == "length" => {
return Some((
Property::data(JsValue::Number(elements.len() as f64)),
false,
));
}
_ => {}
}
}
if let ExoticObject::Map { ref entries } = self.exotic
&& let PropertyKey::String(s) = key
&& s.as_str() == "size"
{
return Some((Property::data(JsValue::Number(entries.len() as f64)), false));
}
if let ExoticObject::Set { ref entries } = self.exotic
&& let PropertyKey::String(s) = key
&& s.as_str() == "size"
{
return Some((Property::data(JsValue::Number(entries.len() as f64)), false));
}
if let ExoticObject::Function(ref func) = self.exotic
&& let PropertyKey::String(s) = key
{
match s.as_str() {
"name" => {
let name = match func {
JsFunction::Native(f) => Some(f.name.clone()),
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => bc
.chunk
.function_info
.as_ref()
.and_then(|info| info.name.clone()),
JsFunction::Bound(b) => {
if let ExoticObject::Function(target_func) = &b.target.borrow().exotic {
match target_func {
JsFunction::Native(f) => {
Some(JsString::from(format!("bound {}", f.name)))
}
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => {
bc.chunk.function_info.as_ref().and_then(|info| {
info.name
.as_ref()
.map(|n| JsString::from(format!("bound {}", n)))
})
}
_ => Some(JsString::from("bound ")),
}
} else {
Some(JsString::from("bound "))
}
}
_ => None,
};
return Some((
Property::with_attributes(
match name {
Some(n) => JsValue::String(n),
None => JsValue::String(JsString::from("")),
},
false, false, true, ),
false,
));
}
"length" => {
let arity = match func {
JsFunction::Native(f) => f.arity,
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => bc
.chunk
.function_info
.as_ref()
.map(|info| info.param_count)
.unwrap_or(0),
JsFunction::Bound(b) => {
if let ExoticObject::Function(target_func) = &b.target.borrow().exotic {
let target_length = match target_func {
JsFunction::Native(f) => f.arity,
JsFunction::Bytecode(bc)
| JsFunction::BytecodeGenerator(bc)
| JsFunction::BytecodeAsync(bc)
| JsFunction::BytecodeAsyncGenerator(bc) => bc
.chunk
.function_info
.as_ref()
.map(|info| info.param_count)
.unwrap_or(0),
_ => 0,
};
target_length.saturating_sub(b.bound_args.len())
} else {
0
}
}
_ => 0,
};
return Some((
Property::with_attributes(
JsValue::Number(arity as f64),
false, false, true, ),
false,
));
}
_ => {}
}
}
if let ExoticObject::Enum(ref data) = self.exotic {
match key {
PropertyKey::String(s) => {
if let Some(val) = data.get_by_name(s.as_str()) {
return Some((Property::data(val), false));
}
if let Ok(n) = s.as_str().parse::<f64>()
&& let Some(name) = data.get_by_value(n)
{
return Some((Property::data(JsValue::String(name)), false));
}
}
PropertyKey::Index(idx) => {
if let Some(name) = data.get_by_value(*idx as f64) {
return Some((Property::data(JsValue::String(name)), false));
}
}
PropertyKey::Symbol(_) => {}
}
}
if let Some(prop) = self.properties.get(key) {
return Some((prop.clone(), false));
}
if let Some(ref proto) = self.prototype
&& let Some((prop, _)) = proto.borrow().get_property_descriptor(key)
{
return Some((prop, true));
}
None
}
pub fn set_property(&mut self, key: PropertyKey, value: JsValue) {
if self.frozen {
return;
}
if let ExoticObject::Array { ref mut elements } = self.exotic {
if let PropertyKey::Index(idx) = key {
let idx = idx as usize;
if idx >= elements.len() {
elements.resize(idx + 1, JsValue::Undefined);
}
if let Some(slot) = elements.get_mut(idx) {
*slot = value;
}
return;
}
if let PropertyKey::String(ref s) = key
&& s.as_str() == "length"
{
if let JsValue::Number(n) = value {
let new_len = n as usize;
elements.resize(new_len, JsValue::Undefined);
}
return;
}
}
if let ExoticObject::Enum(ref mut data) = self.exotic
&& let PropertyKey::String(ref s) = key
{
if data.set_by_name(s.as_str(), value.clone()) {
if let JsValue::Number(n) = &value {
let reverse_key =
if math::fract(*n) == 0.0 && *n >= 0.0 && *n <= u32::MAX as f64 {
PropertyKey::Index(*n as u32)
} else {
PropertyKey::String(JsString::from(ToString::to_string(n)))
};
self.properties.insert(
reverse_key,
Property::data(JsValue::String(s.cheap_clone())),
);
}
return;
}
}
if let Some(prop) = self.properties.get_mut(&key) {
if prop.writable() {
prop.value = value;
}
} else if self.extensible && !self.sealed {
self.properties.insert(key, Property::data(value));
}
}
pub fn define_property(&mut self, key: PropertyKey, prop: Property) {
self.properties.insert(key, prop);
}
pub fn has_own_property(&self, key: &PropertyKey) -> bool {
self.properties.contains_key(key)
}
pub fn own_keys(&self) -> Vec<PropertyKey> {
self.properties.keys().cloned().collect()
}
#[inline]
pub fn array_length(&self) -> Option<u32> {
if let ExoticObject::Array { ref elements } = self.exotic {
Some(elements.len() as u32)
} else {
None
}
}
#[inline]
pub fn array_elements(&self) -> Option<&[JsValue]> {
if let ExoticObject::Array { ref elements } = self.exotic {
Some(elements)
} else {
None
}
}
#[inline]
pub fn array_elements_mut(&mut self) -> Option<&mut Vec<JsValue>> {
if let ExoticObject::Array { ref mut elements } = self.exotic {
Some(elements)
} else {
None
}
}
#[inline]
pub fn is_array(&self) -> bool {
matches!(self.exotic, ExoticObject::Array { .. })
}
}
impl Default for JsObject {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PropertyKey {
String(JsString),
Index(u32),
Symbol(Box<JsSymbol>),
}
impl PropertyKey {
pub fn from_value(value: &JsValue) -> Self {
match value {
JsValue::Number(n) => {
let idx = *n as u32;
if idx as f64 == *n && *n >= 0.0 {
PropertyKey::Index(idx)
} else {
PropertyKey::String(value.to_js_string())
}
}
JsValue::String(s) => {
if let Ok(idx) = s.parse::<u32>()
&& idx.to_string() == s.as_str()
{
return PropertyKey::Index(idx);
}
PropertyKey::String(s.cheap_clone())
}
JsValue::Symbol(s) => PropertyKey::Symbol(s.clone()),
_ => PropertyKey::String(value.to_js_string()),
}
}
pub fn is_symbol(&self) -> bool {
matches!(self, PropertyKey::Symbol(_))
}
#[inline]
pub fn eq_str(&self, s: &str) -> bool {
match self {
PropertyKey::String(js_str) => js_str.as_str() == s,
PropertyKey::Index(_) | PropertyKey::Symbol(_) => false,
}
}
}
impl fmt::Display for PropertyKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropertyKey::String(s) => write!(f, "{}", s),
PropertyKey::Index(i) => write!(f, "{}", i),
PropertyKey::Symbol(s) => match &s.description {
Some(desc) => write!(f, "Symbol({})", desc.as_str()),
None => write!(f, "Symbol()"),
},
}
}
}
mod property_flags {
pub const WRITABLE: u8 = 0b001;
pub const ENUMERABLE: u8 = 0b010;
pub const CONFIGURABLE: u8 = 0b100;
pub const ALL: u8 = WRITABLE | ENUMERABLE | CONFIGURABLE;
}
#[derive(Debug, Clone)]
pub struct Accessor {
pub getter: Option<JsObjectRef>,
pub setter: Option<JsObjectRef>,
}
#[derive(Debug, Clone)]
pub struct Property {
pub value: JsValue,
flags: u8,
accessor: Option<Box<Accessor>>,
}
impl Property {
#[inline]
pub fn data(value: JsValue) -> Self {
Self {
value,
flags: property_flags::ALL,
accessor: None,
}
}
#[inline]
pub fn data_readonly(value: JsValue) -> Self {
Self {
value,
flags: property_flags::ENUMERABLE | property_flags::CONFIGURABLE,
accessor: None,
}
}
pub fn accessor(getter: Option<JsObjectRef>, setter: Option<JsObjectRef>) -> Self {
Self {
value: JsValue::Undefined,
flags: property_flags::ENUMERABLE | property_flags::CONFIGURABLE,
accessor: Some(Box::new(Accessor { getter, setter })),
}
}
#[inline]
pub fn is_accessor(&self) -> bool {
self.accessor.is_some()
}
#[inline]
pub fn with_attributes(
value: JsValue,
writable: bool,
enumerable: bool,
configurable: bool,
) -> Self {
let mut flags = 0;
if writable {
flags |= property_flags::WRITABLE;
}
if enumerable {
flags |= property_flags::ENUMERABLE;
}
if configurable {
flags |= property_flags::CONFIGURABLE;
}
Self {
value,
flags,
accessor: None,
}
}
#[inline]
pub fn writable(&self) -> bool {
(self.flags & property_flags::WRITABLE) != 0
}
#[inline]
pub fn enumerable(&self) -> bool {
(self.flags & property_flags::ENUMERABLE) != 0
}
#[inline]
pub fn configurable(&self) -> bool {
(self.flags & property_flags::CONFIGURABLE) != 0
}
#[inline]
pub fn set_writable(&mut self, writable: bool) {
if writable {
self.flags |= property_flags::WRITABLE;
} else {
self.flags &= !property_flags::WRITABLE;
}
}
#[inline]
pub fn set_enumerable(&mut self, enumerable: bool) {
if enumerable {
self.flags |= property_flags::ENUMERABLE;
} else {
self.flags &= !property_flags::ENUMERABLE;
}
}
#[inline]
pub fn set_configurable(&mut self, configurable: bool) {
if configurable {
self.flags |= property_flags::CONFIGURABLE;
} else {
self.flags &= !property_flags::CONFIGURABLE;
}
}
#[inline]
pub fn getter(&self) -> Option<&JsObjectRef> {
self.accessor.as_ref().and_then(|a| a.getter.as_ref())
}
#[inline]
pub fn setter(&self) -> Option<&JsObjectRef> {
self.accessor.as_ref().and_then(|a| a.setter.as_ref())
}
pub fn set_getter(&mut self, getter: Option<JsObjectRef>) {
if let Some(ref mut acc) = self.accessor {
acc.getter = getter;
} else if getter.is_some() {
self.accessor = Some(Box::new(Accessor {
getter,
setter: None,
}));
}
}
pub fn set_setter(&mut self, setter: Option<JsObjectRef>) {
if let Some(ref mut acc) = self.accessor {
acc.setter = setter;
} else if setter.is_some() {
self.accessor = Some(Box::new(Accessor {
getter: None,
setter,
}));
}
}
}
const INLINE_PROPERTY_CAPACITY: usize = 2;
#[derive(Debug)]
pub enum PropertyStorage {
Inline {
len: u8,
entries: [(PropertyKey, Property); INLINE_PROPERTY_CAPACITY],
},
Map(FxHashMap<PropertyKey, Property>),
}
impl Default for PropertyStorage {
fn default() -> Self {
Self::new()
}
}
impl PropertyStorage {
#[inline]
pub fn new() -> Self {
PropertyStorage::Inline {
len: 0,
entries: core::array::from_fn(|_| {
(PropertyKey::Index(0), Property::data(JsValue::Undefined))
}),
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
if capacity <= INLINE_PROPERTY_CAPACITY {
Self::new()
} else {
PropertyStorage::Map(FxHashMap::with_capacity_and_hasher(
capacity,
Default::default(),
))
}
}
#[inline]
pub fn reserve(&mut self, additional: usize) {
if let PropertyStorage::Map(map) = self {
map.reserve(additional);
}
}
#[inline]
pub fn get(&self, key: &PropertyKey) -> Option<&Property> {
match self {
PropertyStorage::Inline { len, entries } => {
for entry in entries.get(..(*len as usize)).unwrap_or_default() {
if &entry.0 == key {
return Some(&entry.1);
}
}
None
}
PropertyStorage::Map(map) => map.get(key),
}
}
#[inline]
pub fn get_mut(&mut self, key: &PropertyKey) -> Option<&mut Property> {
match self {
PropertyStorage::Inline { len, entries } => {
for entry in entries.get_mut(..(*len as usize)).unwrap_or_default() {
if &entry.0 == key {
return Some(&mut entry.1);
}
}
None
}
PropertyStorage::Map(map) => map.get_mut(key),
}
}
pub fn insert(&mut self, key: PropertyKey, value: Property) -> Option<Property> {
match self {
PropertyStorage::Inline { len, entries } => {
let current_len = *len as usize;
for entry in entries.get_mut(..current_len).unwrap_or_default() {
if entry.0 == key {
let old = mem::replace(&mut entry.1, value);
return Some(old);
}
}
if let Some(slot) = entries.get_mut(current_len) {
*slot = (key, value);
*len += 1;
return None;
}
let mut map = FxHashMap::with_capacity_and_hasher(
INLINE_PROPERTY_CAPACITY + 1,
Default::default(),
);
for entry in entries.iter_mut() {
let (k, v) = mem::replace(
entry,
(PropertyKey::Index(0), Property::data(JsValue::Undefined)),
);
map.insert(k, v);
}
map.insert(key, value);
*self = PropertyStorage::Map(map);
None
}
PropertyStorage::Map(map) => map.insert(key, value),
}
}
#[inline]
pub fn contains_key(&self, key: &PropertyKey) -> bool {
match self {
PropertyStorage::Inline { len, entries } => {
for entry in entries.get(..(*len as usize)).unwrap_or_default() {
if &entry.0 == key {
return true;
}
}
false
}
PropertyStorage::Map(map) => map.contains_key(key),
}
}
pub fn remove(&mut self, key: &PropertyKey) -> Option<Property> {
match self {
PropertyStorage::Inline { len, entries } => {
let current_len = *len as usize;
let mut found_idx = None;
for (i, entry) in entries
.get(..current_len)
.unwrap_or_default()
.iter()
.enumerate()
{
if &entry.0 == key {
found_idx = Some(i);
break;
}
}
if let Some(i) = found_idx {
let removed = if let Some(entry) = entries.get_mut(i) {
mem::replace(
entry,
(PropertyKey::Index(0), Property::data(JsValue::Undefined)),
)
} else {
return None;
};
if i < current_len - 1 {
entries.swap(i, current_len - 1);
}
*len -= 1;
Some(removed.1)
} else {
None
}
}
PropertyStorage::Map(map) => map.remove(key),
}
}
#[inline]
pub fn clear(&mut self) {
match self {
PropertyStorage::Inline { len, entries } => {
for entry in entries.get_mut(..(*len as usize)).unwrap_or_default() {
*entry = (PropertyKey::Index(0), Property::data(JsValue::Undefined));
}
*len = 0;
}
PropertyStorage::Map(map) => map.clear(),
}
}
#[inline]
pub fn len(&self) -> usize {
match self {
PropertyStorage::Inline { len, .. } => *len as usize,
PropertyStorage::Map(map) => map.len(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn iter(&self) -> PropertyStorageIter<'_> {
match self {
PropertyStorage::Inline { len, entries } => PropertyStorageIter::Inline {
entries,
index: 0,
len: *len as usize,
},
PropertyStorage::Map(map) => PropertyStorageIter::Map(map.iter()),
}
}
pub fn iter_mut(&mut self) -> PropertyStorageIterMut<'_> {
match self {
PropertyStorage::Inline { len, entries } => {
let len = *len as usize;
PropertyStorageIterMut::Inline {
entries: entries.get_mut(..len).unwrap_or_default(),
}
}
PropertyStorage::Map(map) => PropertyStorageIterMut::Map(map.iter_mut()),
}
}
pub fn keys(&self) -> impl Iterator<Item = &PropertyKey> {
self.iter().map(|(k, _)| k)
}
pub fn values(&self) -> impl Iterator<Item = &Property> {
self.iter().map(|(_, v)| v)
}
}
pub enum PropertyStorageIter<'a> {
Inline {
entries: &'a [(PropertyKey, Property); INLINE_PROPERTY_CAPACITY],
index: usize,
len: usize,
},
#[cfg(feature = "std")]
Map(std::collections::hash_map::Iter<'a, PropertyKey, Property>),
#[cfg(not(feature = "std"))]
Map(hashbrown::hash_map::Iter<'a, PropertyKey, Property>),
}
impl<'a> Iterator for PropertyStorageIter<'a> {
type Item = (&'a PropertyKey, &'a Property);
fn next(&mut self) -> Option<Self::Item> {
match self {
PropertyStorageIter::Inline {
entries,
index,
len,
} => {
if *index < *len {
let i = *index;
*index += 1;
entries.get(i).map(|e| (&e.0, &e.1))
} else {
None
}
}
PropertyStorageIter::Map(iter) => iter.next(),
}
}
}
pub enum PropertyStorageIterMut<'a> {
Inline {
entries: &'a mut [(PropertyKey, Property)],
},
#[cfg(feature = "std")]
Map(std::collections::hash_map::IterMut<'a, PropertyKey, Property>),
#[cfg(not(feature = "std"))]
Map(hashbrown::hash_map::IterMut<'a, PropertyKey, Property>),
}
impl<'a> Iterator for PropertyStorageIterMut<'a> {
type Item = (&'a PropertyKey, &'a mut Property);
fn next(&mut self) -> Option<Self::Item> {
match self {
PropertyStorageIterMut::Inline { entries } => {
if entries.is_empty() {
None
} else {
let (first, rest) = mem::take(entries).split_at_mut(1);
*entries = rest;
first.first_mut().map(|e| (&e.0, &mut e.1))
}
}
PropertyStorageIterMut::Map(iter) => iter.next(),
}
}
}
#[derive(Debug)]
pub struct EnvironmentData {
pub bindings: FxHashMap<VarKey, Binding>,
pub outer: Option<JsObjectRef>,
}
impl EnvironmentData {
pub fn new() -> Self {
Self {
bindings: FxHashMap::default(),
outer: None,
}
}
pub fn with_outer(outer: Option<JsObjectRef>) -> Self {
Self {
bindings: FxHashMap::default(),
outer,
}
}
pub fn with_outer_and_capacity(outer: Option<JsObjectRef>, capacity: usize) -> Self {
Self {
bindings: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
outer,
}
}
}
impl Default for EnvironmentData {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct JsMapKey(pub JsValue);
impl JsMapKey {
fn same_value_zero(a: &JsValue, b: &JsValue) -> bool {
match (a, b) {
(JsValue::Number(x), JsValue::Number(y)) => {
if x.is_nan() && y.is_nan() {
return true;
}
x == y
}
(JsValue::String(x), JsValue::String(y)) => x == y,
(JsValue::Boolean(x), JsValue::Boolean(y)) => x == y,
(JsValue::Null, JsValue::Null) => true,
(JsValue::Undefined, JsValue::Undefined) => true,
(JsValue::Object(x), JsValue::Object(y)) => x.id() == y.id(),
(JsValue::Symbol(x), JsValue::Symbol(y)) => x.id == y.id,
_ => false,
}
}
}
impl PartialEq for JsMapKey {
fn eq(&self, other: &Self) -> bool {
Self::same_value_zero(&self.0, &other.0)
}
}
impl Eq for JsMapKey {}
impl Hash for JsMapKey {
fn hash<H: Hasher>(&self, state: &mut H) {
mem::discriminant(&self.0).hash(state);
match &self.0 {
JsValue::Undefined | JsValue::Null => {
}
JsValue::Boolean(b) => b.hash(state),
JsValue::Number(n) => {
if n.is_nan() {
0x7FF8_0000_0000_0000u64.hash(state); } else if *n == 0.0 {
0u64.hash(state); } else {
n.to_bits().hash(state);
}
}
JsValue::String(s) => s.hash(state),
JsValue::Symbol(sym) => sym.id.hash(state),
JsValue::Object(obj) => obj.id().hash(state),
}
}
}
#[derive(Debug)]
pub enum ExoticObject {
Ordinary,
Array { elements: Vec<JsValue> },
Boolean(bool),
Number(f64),
StringObj(JsString),
Symbol(Box<JsSymbol>),
Function(JsFunction),
Map {
entries: IndexMap<JsMapKey, JsValue>,
},
Set { entries: IndexSet<JsMapKey> },
Date { timestamp: f64 },
RegExp {
pattern: String,
flags: String,
compiled: Option<Rc<dyn CompiledRegex>>,
},
Generator(Rc<RefCell<GeneratorState>>),
BytecodeGenerator(Rc<RefCell<BytecodeGeneratorState>>),
Promise(Rc<RefCell<PromiseState>>),
Environment(EnvironmentData),
Enum(EnumData),
Proxy(ProxyData),
RawJSON(JsString),
PendingOrder { id: u64 },
}
#[derive(Debug, Clone)]
pub struct ProxyData {
pub target: JsObjectRef,
pub handler: JsObjectRef,
pub revoked: bool,
}
#[derive(Debug, Clone)]
pub struct EnumMember {
pub name: JsString,
pub value: JsValue,
}
#[derive(Debug, Clone)]
pub struct EnumData {
pub name: JsString,
pub const_: bool,
pub members: Vec<EnumMember>,
}
impl EnumData {
pub fn get_by_name(&self, name: &str) -> Option<JsValue> {
self.members
.iter()
.find(|m| m.name.as_str() == name)
.map(|m| m.value.clone())
}
pub fn get_by_value(&self, value: f64) -> Option<JsString> {
self.members.iter().find_map(|m| {
if let JsValue::Number(n) = &m.value
&& *n == value
{
return Some(m.name.cheap_clone());
}
None
})
}
pub fn keys(&self) -> Vec<PropertyKey> {
let mut keys = Vec::with_capacity(self.members.len() * 2);
for member in &self.members {
keys.push(PropertyKey::String(member.name.cheap_clone()));
if let JsValue::Number(_) = &member.value {
keys.push(PropertyKey::from_value(&member.value));
}
}
keys
}
pub fn values(&self) -> Vec<JsValue> {
let mut values = Vec::with_capacity(self.members.len() * 2);
for member in &self.members {
values.push(member.value.clone());
if let JsValue::Number(_) = &member.value {
values.push(JsValue::String(member.name.cheap_clone()));
}
}
values
}
pub fn entries(&self) -> Vec<(String, JsValue)> {
let mut entries = Vec::with_capacity(self.members.len() * 2);
for member in &self.members {
entries.push((member.name.to_string(), member.value.clone()));
if let JsValue::Number(n) = &member.value {
entries.push((n.to_string(), JsValue::String(member.name.cheap_clone())));
}
}
entries
}
pub fn has_property(&self, key: &PropertyKey) -> bool {
match key {
PropertyKey::String(s) => {
if self.members.iter().any(|m| m.name.as_str() == s.as_str()) {
return true;
}
if let Ok(n) = s.as_str().parse::<f64>() {
return self.get_by_value(n).is_some();
}
false
}
PropertyKey::Index(idx) => self.get_by_value(*idx as f64).is_some(),
PropertyKey::Symbol(_) => false,
}
}
pub fn set_by_name(&mut self, name: &str, value: JsValue) -> bool {
if let Some(member) = self.members.iter_mut().find(|m| m.name.as_str() == name) {
member.value = value;
true
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct PromiseState {
pub status: PromiseStatus,
pub result: Option<JsValue>,
pub handlers: Vec<PromiseHandler>,
pub order_id: Option<crate::OrderId>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PromiseStatus {
Pending,
Fulfilled,
Rejected,
}
#[derive(Debug, Clone)]
pub struct PromiseHandler {
pub on_fulfilled: Option<JsValue>,
pub on_rejected: Option<JsValue>,
pub result_promise: JsObjectRef,
}
#[derive(Debug, Clone)]
pub struct SavedFrameState {
pub frame_data: Vec<u8>,
pub frame_count: usize,
}
pub struct GeneratorState {
pub body: Rc<BlockStatement>,
pub params: Rc<[FunctionParam]>,
pub args: Vec<JsValue>,
pub closure: JsObjectRef,
pub status: GeneratorStatus,
pub sent_value: JsValue,
pub name: Option<JsString>,
pub id: u64,
pub started: bool,
}
impl fmt::Debug for GeneratorState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GeneratorState")
.field("status", &self.status)
.field("id", &self.id)
.field("started", &self.started)
.finish()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum GeneratorStatus {
Suspended,
Completed,
}
pub struct BytecodeGeneratorState {
pub chunk: Rc<crate::compiler::BytecodeChunk>,
pub closure: JsObjectRef,
pub args: Vec<JsValue>,
pub this_value: JsValue,
pub status: GeneratorStatus,
pub sent_value: JsValue,
pub id: u64,
pub started: bool,
pub saved_ip: usize,
pub saved_registers: Vec<JsValue>,
pub saved_call_stack: Vec<crate::interpreter::bytecode_vm::CallFrame>,
pub saved_try_stack: Vec<crate::interpreter::bytecode_vm::TryHandler>,
pub yield_result_register: Option<u8>,
pub func_env: Option<JsObjectRef>,
pub current_env: Option<JsObjectRef>,
pub delegated_iterator: Option<(JsObjectRef, JsValue)>,
pub is_async: bool,
pub throw_value: Option<JsValue>,
}
impl fmt::Debug for BytecodeGeneratorState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BytecodeGeneratorState")
.field("status", &self.status)
.field("id", &self.id)
.field("started", &self.started)
.field("saved_ip", &self.saved_ip)
.finish()
}
}
#[derive(Debug, Clone)]
pub enum JsFunction {
Bytecode(BytecodeFunction),
BytecodeGenerator(BytecodeFunction),
BytecodeAsync(BytecodeFunction),
BytecodeAsyncGenerator(BytecodeFunction),
Native(NativeFunction),
Bound(Box<BoundFunctionData>),
PromiseResolve(JsObjectRef),
PromiseReject(JsObjectRef),
PromiseAllFulfill {
state: Rc<PromiseAllSharedState>,
index: usize,
},
PromiseAllReject(Rc<PromiseAllSharedState>),
PromiseRaceSettle {
state: Rc<PromiseRaceSharedState>,
is_fulfill: bool,
index: usize,
},
AccessorGetter,
AccessorSetter,
ModuleExportGetter {
module_env: JsObjectRef,
binding_name: JsString,
},
ModuleReExportGetter {
source_module: JsObjectRef,
source_key: PropertyKey,
},
ProxyRevoke(JsObjectRef),
}
#[derive(Debug)]
pub struct PromiseAllSharedState {
pub remaining: Cell<usize>,
pub results: RefCell<Vec<JsValue>>,
pub result_promise: JsObjectRef,
pub rejected: Cell<bool>,
}
#[derive(Debug)]
pub struct PromiseRaceSharedState {
pub result_promise: JsObjectRef,
pub settled: Cell<bool>,
pub input_order_ids: Vec<Option<crate::OrderId>>,
}
#[derive(Debug, Clone)]
pub struct BoundFunctionData {
pub target: JsObjectRef,
pub this_arg: JsValue,
pub bound_args: Vec<JsValue>,
}
impl JsFunction {
pub fn name(&self) -> Option<&str> {
match self {
JsFunction::Bytecode(f)
| JsFunction::BytecodeGenerator(f)
| JsFunction::BytecodeAsync(f)
| JsFunction::BytecodeAsyncGenerator(f) => f
.chunk
.function_info
.as_ref()
.and_then(|info| info.name.as_ref())
.map(|s| s.as_str()),
JsFunction::Native(f) => Some(f.name.as_ref()),
JsFunction::Bound(_) => Some("bound"),
JsFunction::PromiseResolve(_) => Some("resolve"),
JsFunction::PromiseReject(_) => Some("reject"),
JsFunction::PromiseAllFulfill { .. } => Some("promiseAllFulfill"),
JsFunction::PromiseAllReject(_) => Some("promiseAllReject"),
JsFunction::PromiseRaceSettle { .. } => Some("promiseRaceSettle"),
JsFunction::AccessorGetter => Some("get"),
JsFunction::AccessorSetter => Some("set"),
JsFunction::ModuleExportGetter { .. } => Some("get"),
JsFunction::ModuleReExportGetter { .. } => Some("get"),
JsFunction::ProxyRevoke(_) => Some("revoke"),
}
}
}
#[derive(Debug, Clone)]
pub struct BytecodeFunction {
pub chunk: Rc<crate::compiler::BytecodeChunk>,
pub closure: JsObjectRef,
pub captured_this: Option<Box<JsValue>>,
}
pub type NativeFn =
fn(&mut crate::interpreter::Interpreter, JsValue, &[JsValue]) -> Result<Guarded, JsError>;
#[derive(Clone)]
pub struct NativeFunction {
pub name: JsString,
pub func: NativeFn,
pub arity: usize,
pub ffi_id: usize,
}
impl fmt::Debug for NativeFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NativeFunction")
.field("name", &self.name)
.field("arity", &self.arity)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct Binding {
pub value: JsValue,
pub mutable: bool,
pub initialized: bool,
pub import_binding: Option<ImportBinding>,
}
#[derive(Debug, Clone)]
pub struct ImportBinding {
pub module_obj: JsObjectRef,
pub property_key: PropertyKey,
}
#[derive(Debug, Clone)]
pub enum ModuleExport {
Direct { name: JsString, value: JsValue },
ReExport {
source_module: JsObjectRef,
source_key: PropertyKey,
},
}
pub type EnvRef = JsObjectRef;
pub struct GuardedEnv {
pub env: EnvRef,
pub guard: Option<Guard<JsObject>>,
}
impl GuardedEnv {
pub fn with_guard(env: EnvRef, guard: Guard<JsObject>) -> Self {
Self {
env,
guard: Some(guard),
}
}
pub fn unguarded(env: EnvRef) -> Self {
Self { env, guard: None }
}
pub fn get(&self) -> &EnvRef {
&self.env
}
pub fn clone_ref(&self) -> EnvRef {
self.env.clone()
}
}
pub fn create_environment_with_guard(guard: &Guard<JsObject>, outer: Option<EnvRef>) -> EnvRef {
let env = guard.alloc();
{
let mut env_ref = env.borrow_mut();
env_ref.null_prototype = true;
env_ref.exotic = ExoticObject::Environment(EnvironmentData::with_outer(outer));
}
env
}
pub fn create_environment_unrooted(
heap: &Heap<JsObject>,
outer: Option<EnvRef>,
) -> (EnvRef, Guard<JsObject>) {
let guard = heap.create_guard();
let env = guard.alloc();
{
let mut env_ref = env.borrow_mut();
env_ref.null_prototype = true;
env_ref.exotic = ExoticObject::Environment(EnvironmentData::with_outer(outer));
}
(env, guard)
}
pub fn create_environment_unrooted_with_capacity(
heap: &Heap<JsObject>,
outer: Option<EnvRef>,
capacity: usize,
) -> (EnvRef, Guard<JsObject>) {
let guard = heap.create_guard();
let env = guard.alloc();
{
let mut env_ref = env.borrow_mut();
env_ref.null_prototype = true;
env_ref.exotic =
ExoticObject::Environment(EnvironmentData::with_outer_and_capacity(outer, capacity));
}
(env, guard)
}
pub fn create_guarded_env(heap: &Heap<JsObject>, outer: Option<EnvRef>) -> GuardedEnv {
let (env, guard) = create_environment_unrooted(heap, outer);
GuardedEnv::with_guard(env, guard)
}
impl JsObject {
pub fn as_environment(&self) -> Option<&EnvironmentData> {
match &self.exotic {
ExoticObject::Environment(data) => Some(data),
_ => None,
}
}
pub fn as_environment_mut(&mut self) -> Option<&mut EnvironmentData> {
match &mut self.exotic {
ExoticObject::Environment(data) => Some(data),
_ => None,
}
}
pub fn is_environment(&self) -> bool {
matches!(self.exotic, ExoticObject::Environment(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_boolean() {
assert!(!JsValue::Undefined.to_boolean());
assert!(!JsValue::Null.to_boolean());
assert!(!JsValue::Boolean(false).to_boolean());
assert!(JsValue::Boolean(true).to_boolean());
assert!(!JsValue::Number(0.0).to_boolean());
assert!(JsValue::Number(1.0).to_boolean());
assert!(!JsValue::Number(f64::NAN).to_boolean());
assert!(!JsValue::String(JsString::from("")).to_boolean());
assert!(JsValue::String(JsString::from("hello")).to_boolean());
}
#[test]
fn test_to_number() {
assert!(JsValue::Undefined.to_number().is_nan());
assert_eq!(JsValue::Null.to_number(), 0.0);
assert_eq!(JsValue::Boolean(true).to_number(), 1.0);
assert_eq!(JsValue::Boolean(false).to_number(), 0.0);
assert_eq!(JsValue::Number(42.0).to_number(), 42.0);
assert_eq!(JsValue::String(JsString::from("42")).to_number(), 42.0);
assert!(
JsValue::String(JsString::from("hello"))
.to_number()
.is_nan()
);
}
#[test]
fn test_strict_equals() {
assert!(JsValue::Undefined.strict_equals(&JsValue::Undefined));
assert!(JsValue::Null.strict_equals(&JsValue::Null));
assert!(!JsValue::Undefined.strict_equals(&JsValue::Null));
assert!(JsValue::Number(1.0).strict_equals(&JsValue::Number(1.0)));
assert!(!JsValue::Number(f64::NAN).strict_equals(&JsValue::Number(f64::NAN)));
}
#[test]
fn test_to_js_string_number_canonical() {
assert_eq!(
JsValue::Number(0.0000001).to_js_string().as_str(),
"1e-7",
"0.0000001 should be '1e-7'"
);
assert_eq!(
JsValue::Number(0.1).to_js_string().as_str(),
"0.1",
"0.1 should be '0.1'"
);
assert_eq!(
JsValue::Number(1e21).to_js_string().as_str(),
"1e+21",
"1e21 should be '1e+21'"
);
}
#[test]
fn test_property_key_from_decimal() {
let key = PropertyKey::from_value(&JsValue::Number(0.1));
match key {
PropertyKey::String(s) => assert_eq!(s.as_str(), "0.1"),
PropertyKey::Index(_) => panic!("0.1 should not be an index"),
PropertyKey::Symbol(_) => panic!("0.1 should not be a symbol"),
}
}
#[test]
fn test_rust_parse_leading_decimal() {
let parsed: f64 = ".1".parse().unwrap();
assert_eq!(parsed, 0.1);
}
#[test]
fn test_property_key_from_value_decimal_debug() {
let n = 0.1_f64;
let key = PropertyKey::from_value(&JsValue::Number(n));
match key {
PropertyKey::String(s) => {
assert_eq!(s.as_str(), "0.1");
}
PropertyKey::Index(_) => {
panic!("0.1 should not be an index!");
}
PropertyKey::Symbol(_) => panic!("0.1 should not be a symbol"),
}
}
}