diff --git a/crates/oxide_runtime_api/src/coercion.rs b/crates/oxide_runtime_api/src/coercion.rs index 9daf5feb..5324fc60 100644 --- a/crates/oxide_runtime_api/src/coercion.rs +++ b/crates/oxide_runtime_api/src/coercion.rs @@ -87,6 +87,10 @@ pub fn bigint_to_string(v: &num_bigint::BigInt) -> String { /// /// Number/String/Boolean/null 按规范转换;undefined 与不可解析字符串为 `NaN`; /// Object 与 Symbol 不在此处理,返回 `NaN`(对象需走 [`to_number_full`])。 +/// +/// # 边界与前提 +/// 本函数无异常通道,Symbol 的 `TypeError`(ECMA-262 §7.1.4)由调用方的 +/// ToNumber 入口先行拦截,不得把 Symbol 传入后取 `NaN` 当结果。 pub fn to_number(val: JsValue) -> f64 { match val.js_type() { JsType::Int => val.as_int() as f64, @@ -350,6 +354,10 @@ pub fn push_to_string(val: JsValue, buf: &mut String) { /// /// 数值走 ECMA-262 Number::toString 格式化;Object 在此返回占位符 `[object]`, /// 完整路径见 [`to_string_full`]。 +/// +/// # 边界与前提 +/// 本函数无异常通道,Symbol 的 `TypeError`(ECMA-262 §7.1.17)由调用方的 +/// ToString 入口先行拦截,不得把 Symbol 传入后取结果。 pub fn to_string(val: JsValue) -> String { if val.is_int() { return val.as_int().to_string(); @@ -381,6 +389,9 @@ pub fn to_string(val: JsValue) -> String { /// 把任意值转为 UTF-16 单元序列(`ToPrimitive(string hint)` 的单元口径): /// 字符串值直接借出单元(Flat 惰性编码、rope 扁平化);Symbol 返回 TypeError /// 错误;其余值经 ToString 原始值路径后 UTF-16 编码。 +/// +/// # 边界与前提 +/// 非字符串原始值经 [`to_string`] 编码,故调用方须已排除 Symbol(§7.1.17)。 pub fn to_units_full(val: JsValue, host: &mut H) -> Result, String> { if val.is_string() { return Ok(host.string_units(val).into_owned()); @@ -399,6 +410,10 @@ pub fn to_units_full(val: JsValue, host: &mut H) -> Result, /// 把值追加其 UTF-16 单元到 `buf`:字符串值直接扩单元序列,非字符串值经 /// ToString 后 UTF-16 编码。字符串拼接的单元通道路径(不可 lossy)。 +/// +/// # 边界与前提 +/// 本函数无异常通道,Symbol 的 `TypeError`(ECMA-262 §7.1.17)由调用方的 +/// 字符串拼接入口先行拦截,不得把 Symbol 传入(否则被静默丢弃为空单元)。 pub fn push_units_to(val: JsValue, buf: &mut Vec) { if val.is_string() { // SAFETY: val 为字符串值,借用即时消费。 @@ -954,22 +969,7 @@ pub fn well_known_symbol_id(host: &H, ptr: *mut JsObject) -> /// well-known symbol id 的描述名(如 id 9 对应 `Symbol.toStringTag`)。 pub fn well_known_symbol_name(id: u32) -> Option<&'static str> { - Some(match id { - 0 => "Symbol.iterator", - 1 => "Symbol.match", - 2 => "Symbol.replace", - 3 => "Symbol.search", - 4 => "Symbol.split", - 5 => "Symbol.toPrimitive", - 6 => "Symbol.hasInstance", - 7 => "Symbol.matchAll", - 8 => "Symbol.asyncIterator", - 9 => "Symbol.toStringTag", - 10 => "Symbol.species", - 11 => "Symbol.asyncDispose", - 12 => "Symbol.dispose", - _ => return None, - }) + oxide_types::private_key::WELL_KNOWN_SYMBOL_NAMES.get(id as usize).copied() } #[cfg(test)] diff --git a/crates/oxide_types/src/private_key.rs b/crates/oxide_types/src/private_key.rs index 118ffeff..f66314cb 100644 --- a/crates/oxide_types/src/private_key.rs +++ b/crates/oxide_types/src/private_key.rs @@ -56,6 +56,54 @@ pub const SYMBOL_KEY_BASE: u32 = 0xA000_0000; /// `Symbol.asyncDispose` 与 `Symbol.dispose`(显式资源管理提案)。 pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 13; +/// well-known symbol 的名称表,索引即 well-known symbol id。 +/// +/// 表序与 `id` 严格一致,供 Symbol 构造器属性绑定与符号描述查询共用;新增 +/// well-known symbol 时只改此处(连同 [`WELL_KNOWN_SYMBOL_COUNT`] 与具名 id 常量), +/// 避免 id/名映射在多处各自维护造成错位。 +pub const WELL_KNOWN_SYMBOL_NAMES: [&str; WELL_KNOWN_SYMBOL_COUNT as usize] = [ + "Symbol.iterator", + "Symbol.match", + "Symbol.replace", + "Symbol.search", + "Symbol.split", + "Symbol.toPrimitive", + "Symbol.hasInstance", + "Symbol.matchAll", + "Symbol.asyncIterator", + "Symbol.toStringTag", + "Symbol.species", + "Symbol.asyncDispose", + "Symbol.dispose", +]; + +/// well-known symbol 具名 id:`0`(键值 = `SYMBOL_KEY_BASE + id`)。 +pub const WELL_KNOWN_SYMBOL_ITERATOR: u32 = 0; +/// well-known symbol 具名 id:`Symbol.match`。 +pub const WELL_KNOWN_SYMBOL_MATCH: u32 = 1; +/// well-known symbol 具名 id:`Symbol.replace`。 +pub const WELL_KNOWN_SYMBOL_REPLACE: u32 = 2; +/// well-known symbol 具名 id:`Symbol.search`。 +pub const WELL_KNOWN_SYMBOL_SEARCH: u32 = 3; +/// well-known symbol 具名 id:`Symbol.split`。 +pub const WELL_KNOWN_SYMBOL_SPLIT: u32 = 4; +/// well-known symbol 具名 id:`Symbol.toPrimitive`。 +pub const WELL_KNOWN_SYMBOL_TO_PRIMITIVE: u32 = 5; +/// well-known symbol 具名 id:`Symbol.hasInstance`。 +pub const WELL_KNOWN_SYMBOL_HAS_INSTANCE: u32 = 6; +/// well-known symbol 具名 id:`Symbol.matchAll`。 +pub const WELL_KNOWN_SYMBOL_MATCH_ALL: u32 = 7; +/// well-known symbol 具名 id:`Symbol.asyncIterator`。 +pub const WELL_KNOWN_SYMBOL_ASYNC_ITERATOR: u32 = 8; +/// well-known symbol 具名 id:`Symbol.toStringTag`。 +pub const WELL_KNOWN_SYMBOL_TO_STRING_TAG: u32 = 9; +/// well-known symbol 具名 id:`Symbol.species`。 +pub const WELL_KNOWN_SYMBOL_SPECIES: u32 = 10; +/// well-known symbol 具名 id:`Symbol.asyncDispose`。 +pub const WELL_KNOWN_SYMBOL_ASYNC_DISPOSE: u32 = 11; +/// well-known symbol 具名 id:`Symbol.dispose`。 +pub const WELL_KNOWN_SYMBOL_DISPOSE: u32 = 12; + /// 用户 symbol 下标的保留位掩码(低 28 位)。 const SYMBOL_INDEX_MASK: u32 = 0x0FFF_FFFF; @@ -83,15 +131,19 @@ pub const fn is_symbol_key(key: u32) -> bool { key >= SYMBOL_KEY_BASE } -/// 把用户 symbol 的表下标编码为属性键。 +/// 把用户 symbol 的全局下标编码为属性键。 /// -/// 键 = Symbol 区间起点 + well-known 预留槽 + 下标(截断到 28 位防加法回绕)。 +/// 传入的是 interner 分配的**全局**下标(用户符号从 [`WELL_KNOWN_SYMBOL_COUNT`] +/// 起,见 VM 的 `SymbolState::intern`),编码时再叠加一次 well-known 预留槽偏移。 +/// 两段偏移使 `[SYMBOL_KEY_BASE + WELL_KNOWN_SYMBOL_COUNT, SYMBOL_KEY_BASE + 2 * +/// WELL_KNOWN_SYMBOL_COUNT)` 成为空档;与 [`symbol_index_from_key`] 减去同一常量, +/// 往返自洽,空档不落任何产键路径。 #[inline] pub const fn make_symbol_key(idx: u32) -> u32 { SYMBOL_KEY_BASE + WELL_KNOWN_SYMBOL_COUNT + (idx & SYMBOL_INDEX_MASK) } -/// 从用户 symbol 键反解表下标(与 [`make_symbol_key`] 互逆)。 +/// 从用户 symbol 键反解全局下标(与 [`make_symbol_key`] 互逆)。 #[inline] pub const fn symbol_index_from_key(key: u32) -> u32 { (key - SYMBOL_KEY_BASE - WELL_KNOWN_SYMBOL_COUNT) & SYMBOL_INDEX_MASK diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index 6dd0de20..f2941859 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -46,23 +46,10 @@ pub fn bind_symbol(core: &Arc, session: &KernelSession, global: &mut ); // well-known symbol 以符号原语绑定,下标与内建符号表(0..WELL_KNOWN_SYMBOL_COUNT) - // 一一对应;键编码由该下标直接推出。 - for (name, id) in [ - ("match", 1u32), - ("replace", 2), - ("search", 3), - ("split", 4), - ("iterator", 0), - ("toPrimitive", 5), - ("hasInstance", 6), - ("matchAll", 7), - ("asyncIterator", 8), - ("toStringTag", 9), - ("species", 10), - ("asyncDispose", 11), - ("dispose", 12), - ] { - bind_well_known_symbol(core, ctor, name, JsValue::symbol(id)); + // 一一对应;名称表是 id/名映射的唯一来源,属性名去掉 `Symbol.` 前缀。 + for (id, full_name) in oxide_types::private_key::WELL_KNOWN_SYMBOL_NAMES.iter().enumerate() { + let name = full_name.strip_prefix("Symbol.").unwrap_or(full_name); + bind_well_known_symbol(core, ctor, name, JsValue::symbol(id as u32)); } bind_constructor!(core, global, "Symbol", ctor_ptr, oxide_builtins::symbol::symbol_constructor::, 1, hash: true); diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index bc0a2d43..a519c384 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -468,7 +468,8 @@ fn bind_iterator_protos(core: &Arc, session: &KernelSession) { oxide_builtins::iterator::iterator_constructor_setter:: as *const (), ); } - let sym_to_string_tag = oxide_types::private_key::make_well_known_symbol_key(9); + let sym_to_string_tag = + oxide_types::private_key::make_well_known_symbol_key(oxide_types::private_key::WELL_KNOWN_SYMBOL_TO_STRING_TAG); if core .shape_forge() .lookup_position(iter_proto.shape_id(), sym_to_string_tag) @@ -487,13 +488,14 @@ fn bind_iterator_protos(core: &Arc, session: &KernelSession) { } // %IteratorPrototype% 的 @@dispose:显式资源管理下用 return 关闭迭代器。 - let sym_dispose = oxide_types::private_key::make_well_known_symbol_key(12); + let dispose_id = oxide_types::private_key::WELL_KNOWN_SYMBOL_DISPOSE; + let sym_dispose = oxide_types::private_key::make_well_known_symbol_key(dispose_id); if core.shape_forge().lookup_position(iter_proto.shape_id(), sym_dispose).is_none() { bind_well_known_method( world, core, iter_proto, - 12, + dispose_id, "[Symbol.dispose]", oxide_builtins::iterator::iterator_dispose:: as *const (), 0, diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index 195bddce..d2d9bad7 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -56,6 +56,37 @@ fn int_mul(a: i32, b: i32) -> JsValue { } } +/// `+` 系运算在数值/字符串分流前的 Symbol 守卫。 +/// +/// 任一操作数为 Symbol 时规范都要求抛 `TypeError`:字符串分支走 ToString、数值 +/// 分支走 ToNumber(ECMA-262 §7.1.4 / §7.1.17)。消息按分流走向取对应文本; +/// 两侧须已过 ToPrimitive。 +/// +/// `return` 直接返回抛出结果,避免已展开到 try 处理器后继续执行后续分支造成二次抛错。 +macro_rules! reject_symbol_add { + ($self:ident, $lhs:expr, $rhs:expr) => {{ + let (lhs, rhs) = ($lhs, $rhs); + if lhs.is_symbol() || rhs.is_symbol() { + let msg = if lhs.is_string() || rhs.is_string() { + "Cannot convert a Symbol value to a string" + } else { + "Cannot convert a Symbol value to a number" + }; + return $self.raise_type_error(msg); + } + }}; +} + +/// 纯数值算术(减/乘/除/模/幂等)的 Symbol 守卫:ToNumber(Symbol) 抛 `TypeError` +/// (ECMA-262 §7.1.4)。仅检查最终原始值,对象操作数须先经 ToPrimitive。 +macro_rules! reject_symbol_number { + ($self:ident, $l:expr, $r:expr) => {{ + if $l.is_symbol() || $r.is_symbol() { + return $self.raise_type_error("Cannot convert a Symbol value to a number"); + } + }}; +} + impl Vm { #[inline(always)] pub(crate) fn dispatch_add(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { @@ -79,6 +110,7 @@ impl Vm { // 必须放在 coerce 之后。 let lhs = self.coerce_primitive_bounded(lv, false)?; let rhs = self.coerce_primitive_bounded(rv, false)?; + reject_symbol_add!(self, lhs, rhs); if lhs.is_string() || rhs.is_string() { self.regs[rd] = self.concat_strings(lhs, rhs); return Ok(()); @@ -208,6 +240,7 @@ impl Vm { // 顺序与左结合 ADD 一致。 let lhs = self.coerce_primitive_bounded(acc, false)?; let rhs = self.coerce_primitive_bounded(next, false)?; + reject_symbol_add!(self, lhs, rhs); if lhs.is_string() || rhs.is_string() { // ── 阶段 2:字符串模式单趟预分配 ── self.regs[rd] = self.concat_n_strings(lhs, rhs, &ops[idx + 1..])?; @@ -248,6 +281,11 @@ impl Vm { for ® in rest { let v = self.regs[reg]; let prim = if v.is_object() { self.coerce_primitive_bounded(v, false)? } else { v }; + // 字符串模式对全部操作数做 ToString,Symbol 按 §7.1.17 抛 TypeError。 + if prim.is_symbol() { + self.raise_type_error("Cannot convert a Symbol value to a string")?; + return Ok(JsValue::undefined()); + } parts.push(prim); } let cap = parts.iter().fold(0usize, |acc, p| { @@ -288,6 +326,7 @@ impl Vm { if prim.is_bigint() { return self.raise_type_error("Cannot convert a BigInt value to a number"); } + reject_symbol_number!(self, prim, prim); let n = coercion::to_number(prim); self.regs[rd] = JsValue::float(n); Ok(()) @@ -311,6 +350,7 @@ impl Vm { } let lhs = self.coerce_primitive_bounded(lv, false)?; let rhs = self.coerce_primitive_bounded(rv, false)?; + reject_symbol_add!(self, lhs, rhs); if lhs.is_string() || rhs.is_string() { self.regs[rd] = self.concat_strings(lhs, rhs); return Ok(()); @@ -349,6 +389,7 @@ impl Vm { } let l = self.coerce_primitive_bounded(lv, false)?; let r = self.coerce_primitive_bounded(rv, false)?; + reject_symbol_number!(self, l, r); if l.is_bigint() && r.is_bigint() { self.regs[rd] = self.new_bigint(self.bigint_value(l) - self.bigint_value(r)); return Ok(()); @@ -381,6 +422,7 @@ impl Vm { } let l = self.coerce_primitive_bounded(lv, false)?; let r = self.coerce_primitive_bounded(rv, false)?; + reject_symbol_number!(self, l, r); if l.is_bigint() && r.is_bigint() { self.regs[rd] = self.new_bigint(self.bigint_value(l) * self.bigint_value(r)); return Ok(()); @@ -417,6 +459,7 @@ impl Vm { } let l = self.coerce_primitive_bounded(lv, false)?; let r = self.coerce_primitive_bounded(rv, false)?; + reject_symbol_number!(self, l, r); if l.is_bigint() && r.is_bigint() { let rv = self.bigint_value(r); if bigint_is_zero(rv) { @@ -465,6 +508,7 @@ impl Vm { } let l = self.coerce_primitive_bounded(lv, false)?; let r = self.coerce_primitive_bounded(rv, false)?; + reject_symbol_number!(self, l, r); if l.is_bigint() && r.is_bigint() { let rv = self.bigint_value(r); if bigint_is_zero(rv) { @@ -509,6 +553,10 @@ impl Vm { } let l = self.coerce_primitive_bounded(lv, false)?; let r = self.coerce_primitive_bounded(rv, false)?; + if l.is_symbol() || r.is_symbol() { + self.raise_type_error("Cannot convert a Symbol value to a number")?; + return Ok(JsValue::undefined()); + } if l.is_bigint() && r.is_bigint() { let base = self.bigint_value(l).clone(); let exp = self.bigint_value(r).clone(); diff --git a/crates/oxide_vm/src/dispatch/member.rs b/crates/oxide_vm/src/dispatch/member.rs index 1c8f19c5..f8555899 100644 --- a/crates/oxide_vm/src/dispatch/member.rs +++ b/crates/oxide_vm/src/dispatch/member.rs @@ -193,6 +193,16 @@ impl Vm { }; let prop_val = self.coerce_primitive_bounded(prop_raw, false)?; let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; + if prop_val.is_symbol() || rhs.is_symbol() { + // 复合成员加法分流同 `+`:字符串分支走 ToString、数值分支走 ToNumber, + // 两者对 Symbol 均抛 TypeError(ECMA-262 §7.1.4 / §7.1.17)。 + let msg = if prop_val.is_string() || rhs.is_string() { + "Cannot convert a Symbol value to a string" + } else { + "Cannot convert a Symbol value to a number" + }; + return self.raise_type_error(msg); + } let new_val = if prop_val.is_string() || rhs.is_string() { let ls = coercion::to_string(prop_val); let rs = coercion::to_string(rhs); diff --git a/crates/oxide_vm/src/vm/dispatch_loop.rs b/crates/oxide_vm/src/vm/dispatch_loop.rs index f0a19d17..797c4dea 100644 --- a/crates/oxide_vm/src/vm/dispatch_loop.rs +++ b/crates/oxide_vm/src/vm/dispatch_loop.rs @@ -31,7 +31,11 @@ macro_rules! binary_arith { // `{valueOf: () => 2n}` 需先 ToPrimitive 再判定),统一放 coerce 之后。 let l = $self.coerce_primitive_bounded(lv, false)?; let r = $self.coerce_primitive_bounded(rv, false)?; - if l.is_bigint() && r.is_bigint() { + if l.is_symbol() || r.is_symbol() { + // ToNumber(Symbol) 抛 TypeError(ECMA-262 §7.1.4),先于 BigInt 混合判定。 + $self.raise_type_error("Cannot convert a Symbol value to a number")?; + $self.regs[$rd] = JsValue::undefined(); + } else if l.is_bigint() && r.is_bigint() { // 包装对象 coerce 后暴露双 BigInt(如 Object(2n) / 2n)。 let lv = $self.bigint_value(l).clone(); let rv = $self.bigint_value(r).clone(); diff --git a/crates/oxide_vm/src/vm/error.rs b/crates/oxide_vm/src/vm/error.rs index c35d0bca..194c21d0 100644 --- a/crates/oxide_vm/src/vm/error.rs +++ b/crates/oxide_vm/src/vm/error.rs @@ -5,15 +5,12 @@ use oxide_runtime_api as coercion; use oxide_types::error::JsError; use oxide_types::object::JsObject; -use oxide_types::private_key::make_well_known_symbol_key; +use oxide_types::private_key::{make_well_known_symbol_key, WELL_KNOWN_SYMBOL_TO_PRIMITIVE}; use oxide_types::value::{JsValue, PTR_MASK}; use super::{format_error_message, js_error_kind, js_error_kind_name, Vm}; use crate::vm_debug; -/// `Symbol.toPrimitive` 的 well-known symbol 下标。 -const TO_PRIMITIVE_SYMBOL_ID: u32 = 5; - impl Vm { /// ToPrimitive 有界版:按规范序把对象转为原始值,失败统一抛 `TypeError`。 /// @@ -51,7 +48,7 @@ impl Vm { // ECMA-262 §7.1.1 step 1:exotic 对象上(沿原型链读取)的 Symbol.toPrimitive 优先于 // OrdinaryToPrimitive;well-known symbol 键恒定,直接取保留槽。 - let sym_si = make_well_known_symbol_key(TO_PRIMITIVE_SYMBOL_ID); + let sym_si = make_well_known_symbol_key(WELL_KNOWN_SYMBOL_TO_PRIMITIVE); let exotic = { let obj = unsafe { &*obj_ptr }; self.ordinary_get(obj, sym_si, value)? @@ -148,8 +145,16 @@ impl Vm { } /// ToNumber 有界版:先按 number hint 做 ToPrimitive,再转 `f64`。 + /// + /// # 边界与前提 + /// 原始值为 Symbol 时按规范(ECMA-262 §7.1.4)抛 `TypeError`,不落入 + /// [`coercion::to_number`] 的宽松快路径(后者对该输入返回 `NaN`)。 pub(crate) fn coerce_number_bounded(&mut self, value: JsValue) -> Result { let primitive = self.coerce_primitive_bounded(value, false)?; + if primitive.is_symbol() { + self.conversion_error("Cannot convert a Symbol value to a number")?; + return Ok(f64::NAN); + } Ok(coercion::to_number(primitive)) } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 017496ff..799c9ae3 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -4,12 +4,10 @@ use oxide_runtime_api::{push_units_to, to_boolean, to_units_full}; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::{ int_key_value, is_int_key, is_private_name_key, is_symbol_key, make_int_key, make_well_known_symbol_key, + WELL_KNOWN_SYMBOL_HAS_INSTANCE, }; use oxide_types::value::JsValue; -/// `Symbol.hasInstance` 的 well-known symbol 下标。 -const HAS_INSTANCE_SYMBOL_ID: u32 = 6; - impl Vm { pub(crate) fn dispatch_new_expression(&mut self, rd: usize, a: usize, b: usize) -> Result { let constructor_reg = a; @@ -319,7 +317,7 @@ impl Vm { return self.raise_type_error("INSTANCEOF right-hand side is not callable"); } - let has_instance_si = make_well_known_symbol_key(HAS_INSTANCE_SYMBOL_ID); + let has_instance_si = make_well_known_symbol_key(WELL_KNOWN_SYMBOL_HAS_INSTANCE); let ctor_obj = unsafe { &*rhs_val.as_js_object_ptr() }; let has_instance_val = self.ordinary_get(ctor_obj, has_instance_si, rhs_val)?; diff --git a/crates/oxide_vm/tests/coercion_tests.rs b/crates/oxide_vm/tests/coercion_tests.rs index 5f177bbd..7da7b6df 100644 --- a/crates/oxide_vm/tests/coercion_tests.rs +++ b/crates/oxide_vm/tests/coercion_tests.rs @@ -138,8 +138,6 @@ fn to_primitive_symbol_hint_invoked() { } // ToNumber:to_number_full 对 Symbol 抛 TypeError。 -// 注意:一元加号路径用 coerce_number_bounded(对 Symbol 不抛),因此运算符层的 -// +Symbol() 得到 NaN;见下个测试。 #[test] fn to_number_full_symbol_throws() { let mut vm = Vm::new(); @@ -149,10 +147,65 @@ fn to_number_full_symbol_throws() { assert!(result.unwrap_err().contains("Symbol"), "expected a Symbol TypeError message"); } -// 运算符层实况:+Symbol 为 NaN(Symbol 抛出未接入算术路径)。 +// 运算符层:一元加号对 Symbol 抛 TypeError(ToNumber(Symbol),§7.1.4)。 #[test] -fn unary_plus_symbol_is_nan() { - assert_eq!(eval("+Symbol('x')"), "NaN"); +fn unary_plus_symbol_throws() { + let err = eval("+Symbol('x')"); + assert!(err.contains("TypeError"), "expected TypeError for unary +symbol, got {err}"); +} + +// 运算符层:二元 `+` 的数值与字符串两条分支对 Symbol 均抛 TypeError。 +#[test] +fn add_symbol_throws_both_branches() { + for source in [ + "Symbol('x') + ''", + "'' + Symbol('x')", + "Symbol('x') + 0", + "0 + Symbol('x')", + "Symbol('x') + Symbol('y')", + "Symbol.iterator + {}", + ] { + let err = eval(source); + assert!(err.contains("TypeError"), "expected TypeError for {source}, got {err}"); + } +} + +// 运算符层:其余数值算术(减/乘/除/模/幂/位运算/复合赋值)对 Symbol 抛 TypeError。 +#[test] +fn numeric_operators_symbol_throw() { + for source in [ + "Symbol('x') - 1", + "Symbol('x') * 2", + "Symbol('x') / 2", + "Symbol('x') % 2", + "Symbol('x') ** 2", + "Symbol('x') | 0", + "(-Symbol('x'))", + "var v = Symbol('x'); v += ''", + "var v = Symbol('x'); v -= 1", + ] { + let err = eval(source); + assert!(err.contains("TypeError"), "expected TypeError for {source}, got {err}"); + } +} + +// Number 构造器对 Symbol 抛 TypeError(ToNumber 语义),不走 NaN 兜底。 +#[test] +fn number_constructor_symbol_throws() { + let err = eval("Number(Symbol('x'))"); + assert!(err.contains("TypeError"), "expected TypeError for Number(symbol), got {err}"); +} + +// String 构造器的 Symbol 专属分支保留:返回 `Symbol(desc)`,不经 ToString。 +// 模板字面量则走 ToString,Symbol 仍抛 TypeError。 +#[test] +fn string_constructor_and_template_symbol() { + let (vm, result) = eval_ty("String(Symbol('x'))").expect("String(Symbol) 求值失败"); + assert_eq!(vm.lookup_str(result).as_deref(), Some("Symbol(x)")); + let (vm, result) = eval_ty("Symbol('x').description").expect("description 求值失败"); + assert_eq!(vm.lookup_str(result).as_deref(), Some("x")); + let err = eval("`v=${Symbol('x')}`"); + assert!(err.contains("TypeError"), "expected TypeError for template symbol, got {err}"); } // 抽象相等:字符串与数字互相强转。 diff --git a/crates/oxide_vm/tests/concat_n_tests.rs b/crates/oxide_vm/tests/concat_n_tests.rs index 98946874..d0d4d7ce 100644 --- a/crates/oxide_vm/tests/concat_n_tests.rs +++ b/crates/oxide_vm/tests/concat_n_tests.rs @@ -96,12 +96,12 @@ fn concat_n_call_side_effect_order() { #[test] fn concat_n_symbol_matches_binary_add_behavior() { - // Symbol 在字符串模式下被静默忽略(既有 concat_strings 行为),CONCAT_N 与二元 - // ADD 结果逐字节一致,不放大不修复。 - let flat = eval_str("var s = Symbol(); 'a' + s + 'b'"); - let binary = eval_str("var s = Symbol(); ('a' + s) + 'b'"); - assert_eq!(flat, binary); - assert_eq!(flat, "ab"); + // Symbol 在字符串模式下按 §7.1.17 抛 TypeError;CONCAT_N 与二元 ADD 行为一致, + // 不放大不遗漏。 + let flat = eval(&mut Vm::new(), "var s = Symbol(); 'a' + s + 'b'").unwrap_err(); + let binary = eval(&mut Vm::new(), "var s = Symbol(); ('a' + s) + 'b'").unwrap_err(); + assert!(flat.contains("TypeError"), "expected TypeError, got {flat}"); + assert!(binary.contains("TypeError"), "expected TypeError, got {binary}"); } #[test]