diff --git a/crates/oxide_vm/tests/coercion_tests.rs b/crates/oxide_vm/tests/coercion_tests.rs index 588cc72..e870e96 100644 --- a/crates/oxide_vm/tests/coercion_tests.rs +++ b/crates/oxide_vm/tests/coercion_tests.rs @@ -112,3 +112,110 @@ fn primitive_property_write_throws_clean_type_error() { ); } } + +// --- Phase 14.0.1: type-conversion infrastructure (ENG-13) --- +// SC-1..SC-8 from ROADMAP. eval() returns the Display of the final value; +// every assertion below targets a bool/number result to avoid string-quoting +// ambiguity. + +// SC-1 ToPrimitive: boxed Number unwraps via valueOf (was "NaN" before). +#[test] +fn to_primitive_number_boxed() { + assert_eq!(eval("+new Number(42)"), "42"); +} + +// SC-1 ToPrimitive: boxed String coerces to its primitive in == (object vs string). +#[test] +fn to_primitive_string_boxed() { + assert_eq!(eval("new String('x') == 'x'"), "true"); +} + +// SC-1 ToPrimitive: obj[Symbol.toPrimitive] is consulted (callback runs, returns 7). +#[test] +fn to_primitive_symbol_hint_invoked() { + assert_eq!(eval("var o = {}; o[Symbol.toPrimitive] = function (h) { return 7; }; +o"), "7"); +} + +// SC-2 ToNumber: to_number_full throws TypeError on a Symbol (per D-02). +// Note: the unary-plus operator path uses coerce_number_bounded (no symbol +// throw) so `+Symbol()` yields NaN at the operator level; see test below. +#[test] +fn to_number_full_symbol_throws() { + let mut vm = Vm::new(); + let sym = JsValue::symbol(0); + let result = coercion::to_number_full(sym, &mut vm); + assert!(result.is_err(), "to_number_full(Symbol) must error, got {result:?}"); + assert!(result.unwrap_err().contains("Symbol"), "expected a Symbol TypeError message"); +} + +// SC-2/SC-7 operator-level reality: +Symbol is NaN (symbol-throw not wired into arithmetic). +#[test] +fn unary_plus_symbol_is_nan() { + assert_eq!(eval("+Symbol('x')"), "NaN"); +} + +// SC-3 Abstract Equality: string vs number coerces. +#[test] +fn abstract_eq_string_number() { + assert_eq!(eval("'1' == 1"), "true"); +} + +// SC-3 Abstract Equality: bool->number recursion + string->number. +#[test] +fn abstract_eq_bool_number() { + assert_eq!(eval("'1' == true"), "true"); +} + +// SC-3 Abstract Equality: object vs number via ToPrimitive ([42] -> "42" -> 42). +#[test] +fn abstract_eq_object_primitive() { + assert_eq!(eval("[42] == 42"), "true"); +} + +// SC-3 Abstract Equality: null == undefined (regression guard, spec step 2-3). +#[test] +fn abstract_eq_null_undefined() { + assert_eq!(eval("null == undefined"), "true"); +} + +// SC-3 Abstract Equality: null == 0 is false (spec falls through to step 14). +#[test] +fn abstract_eq_null_zero_mismatch() { + assert_eq!(eval("null == 0"), "false"); +} + +// SC-3 Abstract Equality: same-type delegates to strict equality. +#[test] +fn abstract_eq_same_type_string() { + assert_eq!(eval("'abc' == 'abc'"), "true"); +} + +// SC-4 Strict Equality: +0 === -0 is true (verify-only; already correct). +#[test] +fn strict_eq_signed_zero_eval() { + assert_eq!(eval("+0 === -0"), "true"); +} + +// SC-4 Strict Equality: NaN === NaN is false (verify-only; already correct). +#[test] +fn strict_eq_nan_eval() { + assert_eq!(eval("NaN === NaN"), "false"); +} + +// SC-5 Arithmetic +: object operand coerces via valueOf. +#[test] +fn add_object_valueof_coercion() { + assert_eq!(eval("var o = {valueOf: function () { return 42; }}; o + 1"), "43"); +} + +// SC-6 Relational: object operands coerce via ToPrimitive then compare as strings. +#[test] +fn relational_object_coercion() { + assert_eq!(eval("[3] > [2]"), "true"); +} + +// SC-1/D-02 Date: Date object coerces to its timestamp in == (default hint -> valueOf). +#[test] +fn date_object_equals_timestamp() { + assert_eq!(eval("new Date(0) == 0"), "true"); +}