diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 6287f9fd..d3603790 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -254,52 +254,79 @@ pub fn to_boolean(val: JsValue) -> bool { false } -pub fn abstract_eq(lhs: JsValue, rhs: JsValue) -> bool { - if lhs.is_null() && rhs.is_undefined() { +/// Whether two values share the same ECMAScript language type. Number treats +/// int- and double-tagged values as one type (both are Number). +fn same_type(a: JsValue, b: JsValue) -> bool { + if a.is_string() && b.is_string() { return true; } - if lhs.is_undefined() && rhs.is_null() { + if (a.is_int() || a.is_double()) && (b.is_int() || b.is_double()) { return true; } - if lhs.is_string() && rhs.is_string() { - return string_value_eq(lhs, rhs); - } - if lhs.is_int() && rhs.is_int() { - return lhs.as_int() == rhs.as_int(); - } - if lhs.is_double() && rhs.is_double() { - return strict_double_eq(lhs.as_double(), rhs.as_double()); - } - if lhs.is_bool() && rhs.is_bool() { - return lhs.as_bool() == rhs.as_bool(); - } - if lhs.is_null() && rhs.is_null() { + if a.is_bool() && b.is_bool() { return true; } - if lhs.is_undefined() && rhs.is_undefined() { + if a.is_null() && b.is_null() { return true; } - if (lhs.is_int() || lhs.is_double()) && (rhs.is_int() || rhs.is_double()) { - return strict_double_eq(to_number(lhs), to_number(rhs)); + if a.is_undefined() && b.is_undefined() { + return true; } - if (lhs.is_int() || lhs.is_double()) && rhs.is_bool() { - return to_number(lhs) == to_number(rhs); + if a.is_object() && b.is_object() { + return true; } - if lhs.is_bool() && (rhs.is_int() || rhs.is_double()) { - return to_number(lhs) == to_number(rhs); - } - if lhs.is_bool() || rhs.is_bool() { - return to_number(lhs) == to_number(rhs); - } - if lhs.is_null() || rhs.is_null() { - return false; - } - if lhs.is_object() || rhs.is_object() { - return lhs.is_object() && rhs.is_object() && lhs.as_ptr() == rhs.as_ptr(); + if a.is_symbol() && b.is_symbol() { + return true; } false } +/// IsLooselyEqual(x, y) — ECMA-262 §7.2.15 (`==`). +/// +/// BigInt steps are omitted (the engine prunes BigInt). Object operands are +/// coerced via ToPrimitive, which may invoke user `valueOf` / `toString` / +/// `@@toPrimitive`; hence the `VmHost` parameter and the `Result` (a thrown +/// TypeError from those callbacks propagates as `Err`). Note: `Object == Symbol` +/// does NOT trigger ToPrimitive (spec steps 11/12 cover only Number/String) and +/// falls through to `false`. +pub fn abstract_eq(lhs: JsValue, rhs: JsValue, host: &mut H) -> Result { + // Step 1: same type -> strict equality. + if same_type(lhs, rhs) { + return Ok(strict_equality(lhs, rhs)); + } + // Steps 2-3: null <-> undefined. + if (lhs.is_null() && rhs.is_undefined()) || (lhs.is_undefined() && rhs.is_null()) { + return Ok(true); + } + // Steps 5-6: Number <-> String. + if (lhs.is_int() || lhs.is_double()) && rhs.is_string() { + return Ok(strict_double_eq(to_number(lhs), to_number(rhs))); + } + if lhs.is_string() && (rhs.is_int() || rhs.is_double()) { + return Ok(strict_double_eq(to_number(lhs), to_number(rhs))); + } + // Step 9: x is Boolean -> compare ToNumber(x). + if lhs.is_bool() { + return abstract_eq(JsValue::float(to_number(lhs)), rhs, host); + } + // Step 10: y is Boolean -> compare ToNumber(y). + if rhs.is_bool() { + return abstract_eq(lhs, JsValue::float(to_number(rhs)), host); + } + // Step 11: x is Number/String, y is Object -> ToPrimitive(y). + if (lhs.is_int() || lhs.is_double() || lhs.is_string()) && rhs.is_object() { + let prim = to_primitive(rhs, ToPrimitiveHint::Default, host)?; + return abstract_eq(lhs, prim, host); + } + // Step 12: x is Object, y is Number/String -> ToPrimitive(x). + if lhs.is_object() && (rhs.is_int() || rhs.is_double() || rhs.is_string()) { + let prim = to_primitive(lhs, ToPrimitiveHint::Default, host)?; + return abstract_eq(prim, rhs, host); + } + // Step 14: otherwise not equal. + Ok(false) +} + pub fn strict_eq(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_int() && rhs.is_int() { return lhs.as_int() == rhs.as_int(); diff --git a/crates/oxide_vm/src/bindings/bind_array.rs b/crates/oxide_vm/src/bindings/bind_array.rs index 3063e68f..1564a556 100644 --- a/crates/oxide_vm/src/bindings/bind_array.rs +++ b/crates/oxide_vm/src/bindings/bind_array.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use oxide_kernel::builtin::ArrayMethods; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use crate::bind_constructor; +use crate::bindings::apply_binding_table; pub fn bind_array(core: &Arc, session: &KernelSession, global: &mut JsObject) { let _array_methods = ArrayMethods { @@ -46,6 +47,20 @@ pub fn bind_array(core: &Arc, session: &KernelSession, global: &mut core.shape_forge().as_ref(), ); + let array_proto_ptr = session.builtin_world().array_proto.as_ptr() as *mut JsObject; + let array_proto = unsafe { &mut *array_proto_ptr }; + apply_binding_table( + session.builtin_world(), + array_proto, + core, + &[("toString", oxide_builtins::array::array_to_string:: as *const (), 0)], + ); + // Built-in prototype methods are non-enumerable (otherwise they leak into for-in). + let si = core.perm_interner().intern("toString").0; + if let Some(pos) = core.shape_forge().lookup_position(array_proto.shape_id(), si) { + array_proto.set_data_meta(pos, PropAttributes::new(true, false, true)); + } + let ctor_ptr = session.builtin_world().array_constructor.as_ptr() as *mut JsObject; bind_constructor!(core, global, "Array", ctor_ptr, oxide_builtins::array::array_constructor::, 1, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_object.rs b/crates/oxide_vm/src/bindings/bind_object.rs index 12a190a7..712ec39d 100644 --- a/crates/oxide_vm/src/bindings/bind_object.rs +++ b/crates/oxide_vm/src/bindings/bind_object.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use oxide_kernel::builtin::ObjectMethods; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use crate::bind_constructor; +use crate::bindings::apply_binding_table; pub fn bind_object(core: &Arc, session: &KernelSession, global: &mut JsObject) { let methods = ObjectMethods { @@ -36,6 +37,29 @@ pub fn bind_object(core: &Arc, session: &KernelSession, global: &mut .builtin_world() .bind_object_methods(&methods, core.perm_interner().as_ref(), core.shape_forge().as_ref()); + let object_proto_ptr = session.builtin_world().object_proto.as_ptr() as *mut JsObject; + let object_proto = unsafe { &mut *object_proto_ptr }; + apply_binding_table( + session.builtin_world(), + object_proto, + core, + &[ + ( + "toString", + oxide_builtins::object::object_proto_to_string:: as *const (), + 0, + ), + ("valueOf", oxide_builtins::object::object_proto_value_of:: as *const (), 0), + ], + ); + // Built-in prototype methods are non-enumerable (otherwise they leak into for-in). + for name in ["toString", "valueOf"] { + let si = core.perm_interner().intern(name).0; + if let Some(pos) = core.shape_forge().lookup_position(object_proto.shape_id(), si) { + object_proto.set_data_meta(pos, PropAttributes::new(true, false, true)); + } + } + let ctor_ptr = session.builtin_world().object_constructor.as_ptr() as *mut JsObject; bind_constructor!( core, diff --git a/crates/oxide_vm/src/dispatch/comparison.rs b/crates/oxide_vm/src/dispatch/comparison.rs index c312dceb..dc831711 100644 --- a/crates/oxide_vm/src/dispatch/comparison.rs +++ b/crates/oxide_vm/src/dispatch/comparison.rs @@ -5,69 +5,83 @@ use oxide_types::value::JsValue; impl Vm { #[inline(always)] - pub(crate) fn dispatch_eq(&mut self, rd: usize, a: usize, b: usize) { + pub(crate) fn dispatch_eq(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("EQ rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]); - let eq = coercion::abstract_eq(self.regs[a], self.regs[b]); + let lhs = self.regs[a]; + let rhs = self.regs[b]; + let eq = coercion::abstract_eq(lhs, rhs, self)?; self.regs[rd] = JsValue::bool(eq); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_neq(&mut self, rd: usize, a: usize, b: usize) { + pub(crate) fn dispatch_neq(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("NEQ rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]); - let ne = !coercion::abstract_eq(self.regs[a], self.regs[b]); + let lhs = self.regs[a]; + let rhs = self.regs[b]; + let ne = !coercion::abstract_eq(lhs, rhs, self)?; self.regs[rd] = JsValue::bool(ne); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_lt(&mut self, rd: usize, a: usize, b: usize) { + pub(crate) fn dispatch_lt(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("LT rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]); - let rel = coercion::relational_compare(self.regs[a], self.regs[b]); - match rel { + let va = self.coerce_primitive_bounded(self.regs[a], false)?; + let vb = self.coerce_primitive_bounded(self.regs[b], false)?; + match coercion::relational_compare(va, vb) { Some(r) => self.regs[rd] = JsValue::bool(r), None => { vm_trace!("LT incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]); self.regs[rd] = JsValue::bool(false); } } + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_gt(&mut self, rd: usize, a: usize, b: usize) { + pub(crate) fn dispatch_gt(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("GT rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]); - let rel = coercion::relational_compare(self.regs[b], self.regs[a]); - match rel { + let va = self.coerce_primitive_bounded(self.regs[a], false)?; + let vb = self.coerce_primitive_bounded(self.regs[b], false)?; + match coercion::relational_compare(vb, va) { Some(r) => self.regs[rd] = JsValue::bool(r), None => { vm_trace!("GT incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]); self.regs[rd] = JsValue::bool(false); } } + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_lte(&mut self, rd: usize, a: usize, b: usize) { + pub(crate) fn dispatch_lte(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("LTE rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]); - let rel = coercion::relational_compare(self.regs[b], self.regs[a]); - match rel { + let va = self.coerce_primitive_bounded(self.regs[a], false)?; + let vb = self.coerce_primitive_bounded(self.regs[b], false)?; + match coercion::relational_compare(vb, va) { Some(r) => self.regs[rd] = JsValue::bool(!r), None => { vm_trace!("LTE incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]); self.regs[rd] = JsValue::bool(false); } } + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_gte(&mut self, rd: usize, a: usize, b: usize) { + pub(crate) fn dispatch_gte(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("GTE rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]); - let rel = coercion::relational_compare(self.regs[a], self.regs[b]); - match rel { + let va = self.coerce_primitive_bounded(self.regs[a], false)?; + let vb = self.coerce_primitive_bounded(self.regs[b], false)?; + match coercion::relational_compare(va, vb) { Some(r) => self.regs[rd] = JsValue::bool(!r), None => { vm_trace!("GTE incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]); self.regs[rd] = JsValue::bool(false); } } + Ok(()) } #[inline(always)] diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 8b694996..9e69089f 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -830,27 +830,27 @@ impl Vm { } OpCode::EQ => { - self.dispatch_eq(rd, a, b); + self.dispatch_eq(rd, a, b)?; } OpCode::NEQ => { - self.dispatch_neq(rd, a, b); + self.dispatch_neq(rd, a, b)?; } OpCode::LT => { - self.dispatch_lt(rd, a, b); + self.dispatch_lt(rd, a, b)?; } OpCode::GT => { - self.dispatch_gt(rd, a, b); + self.dispatch_gt(rd, a, b)?; } OpCode::LTE => { - self.dispatch_lte(rd, a, b); + self.dispatch_lte(rd, a, b)?; } OpCode::GTE => { - self.dispatch_gte(rd, a, b); + self.dispatch_gte(rd, a, b)?; } OpCode::STRICT_EQ => {