fix: 符号原语数值与字符串转换改抛类型错误

This commit is contained in:
2026-09-17 23:32:28 +00:00
parent 5768febadb
commit 4d0f11e30a
11 changed files with 219 additions and 60 deletions
+4 -17
View File
@@ -46,23 +46,10 @@ pub fn bind_symbol(core: &Arc<KernelCore>, 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::<crate::vm::Vm>, 1, hash: true);
+5 -3
View File
@@ -468,7 +468,8 @@ fn bind_iterator_protos(core: &Arc<KernelCore>, session: &KernelSession) {
oxide_builtins::iterator::iterator_constructor_setter::<crate::vm::Vm> 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<KernelCore>, 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::<crate::vm::Vm> as *const (),
0,
@@ -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 &reg 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();
+10
View File
@@ -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);
+5 -1
View File
@@ -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();
+10 -5
View File
@@ -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<f64, String> {
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))
}
+2 -4
View File
@@ -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<bool, String> {
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)?;