fix: 复合赋值旧值读改抛错读并补严格隐式全局写检查

This commit is contained in:
2026-10-19 01:12:24 +00:00
parent 8c87f5e5fa
commit 673d007b79
13 changed files with 382 additions and 77 deletions
+87 -8
View File
@@ -1,8 +1,10 @@
use crate::vm::Vm;
use crate::{vm_error, vm_trace};
use oxide_bytecode::opcode;
use oxide_runtime_api as coercion;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::arguments_map::ArgumentsMapState;
use oxide_types::mem::P;
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::private_key::{encode_symbol_key, make_int_key};
use oxide_types::value::{JsType, JsValue};
@@ -22,6 +24,28 @@ impl Vm {
}
}
/// 全局对象属性读(含访问器 getter 调用):命中属性时若为访问器则以
/// 全局对象为 receiver 调用 getter(getter 副作用先于后续写生效),返回
/// getter 结果;命中数据属性返回槽值;未命中返回 None。getter 抛错经
/// `raise_call_error` 就地展开(pc 已改写),调用方须按 pc 变化停行。
fn global_prop_read_with_getter(&mut self, global: &P<JsObject>, si: u32) -> Result<Option<JsValue>, String> {
let Some(val) = self.resolve_property(global, si) else {
return Ok(None);
};
let Some(getter) = self.resolve_property_getter(global, si) else {
return Ok(Some(val));
};
// SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。
let global_val = JsValue::from_js_object(global.as_ptr() as *mut JsObject);
match self.call_function_sync(getter, global_val, &[]) {
Ok(v) => Ok(Some(v)),
Err(err) => {
self.raise_call_error(&err)?;
Ok(None)
}
}
}
/// 未声明标识符读:查 global object 属性存在性,命中取属性值,未命中抛 ReferenceError。
///
/// # 步骤
@@ -44,7 +68,14 @@ impl Vm {
let session = self.realm.session.borrow();
session.global_object().clone()
};
if let Some(val) = self.resolve_property(&global, si) {
let pc_before = self.pc;
let hit = self.global_prop_read_with_getter(&global, si)?;
if self.pc != pc_before {
// getter 在途抛错已展开到 catch/finally 入口:pc 由处理器接管,
// 不得续行(结果寄存器不得再写、不得抛未解析引用)。
return Ok(true);
}
if let Some(val) = hit {
self.regs[rd] = val;
Ok(false)
} else {
@@ -71,9 +102,18 @@ impl Vm {
return Err(format!("LOAD_GLOBAL_TYPEOF constant index {idx} is not a string key"));
}
let si = self.property_key_si(key_val)?;
let session = self.realm.session.borrow();
let global = session.global_object();
self.regs[rd] = self.resolve_property(global, si).unwrap_or(JsValue::undefined());
let global = {
let session = self.realm.session.borrow();
session.global_object().clone()
};
// typeof 访问器属性同样触发 getter(getter 抛错时 typeof 不豁免,异常照抛)。
let pc_before = self.pc;
let hit = self.global_prop_read_with_getter(&global, si)?;
if self.pc != pc_before {
// getter 在途抛错已展开:pc 由处理器接管,不得续行。
return Ok(());
}
self.regs[rd] = hit.unwrap_or(JsValue::undefined());
Ok(())
}
@@ -105,12 +145,19 @@ impl Vm {
self.regs[rd] = val;
return Ok(false);
}
// 回退全局对象属性读(未声明名与 known builtin 名同形 LOAD_GLOBAL)。
let global_hit = {
// 回退全局对象属性读(未声明名与 known builtin 名同形 LOAD_GLOBAL);
// 访问器属性调用 getter(receiver = 全局对象),裸读必须触发 getter。
let global = {
let session = self.realm.session.borrow();
let global = session.global_object().clone();
self.resolve_property(&global, si)
session.global_object().clone()
};
let pc_before = self.pc;
let global_hit = self.global_prop_read_with_getter(&global, si)?;
if self.pc != pc_before {
// getter 在途抛错已展开到 catch/finally 入口:pc 由处理器接管,
// 不得续行(结果寄存器不得再写、不得抛未解析引用)。
return Ok(true);
}
if let Some(val) = global_hit {
self.regs[rd] = val;
Ok(false)
@@ -196,6 +243,38 @@ impl Vm {
Ok(())
}
/// ToPropertyKey:推导属性键值。Symbol 原样返回;对象经 ToPrimitive(string
/// hint) 后 ToString;其余原始值直接 ToString。
///
/// # 边界与前提
/// - 对象键的 toString/valueOf 调用有用户副作用,可能抛异常;抛错经
/// `raise_call_error` 就地展开(pc 已改写),调用方须按 pc 变化停行。
///
/// # 副作用
/// - 成功路径返回推导后的字符串/Symbol 值;抛错路径经 `unwind` 改写
/// pc / 异常通道并返回 undefined(调用方不得再使用)。
pub(crate) fn derive_property_key_value(&mut self, val: JsValue) -> Result<JsValue, String> {
vm_trace!("derive_property_key_value: {:?}", val);
if val.is_symbol() {
return Ok(val);
}
let pc_before = self.pc;
let prim = if val.is_object() {
coercion::to_primitive(val, coercion::ToPrimitiveHint::String, self)?
} else {
val
};
if self.pc != pc_before {
// ToPrimitive 在途抛错已展开:pc 由处理器接管,结果值不得使用。
return Ok(JsValue::undefined());
}
if prim.is_symbol() {
return Ok(prim);
}
let units = oxide_runtime_api::to_units_full(prim, self)?;
Ok(self.new_string_units_owned(units))
}
pub(crate) fn dispatch_load_var(&mut self, rd: usize, a: usize) -> Result<bool, String> {
vm_trace!("LOAD_VAR rd={} r{}={:?}", rd, a, self.regs[a]);
// a==254 即读 this:derived 构造帧在 super() 成功前读 this 抛 ReferenceError。
+70 -1
View File
@@ -24,6 +24,7 @@ impl Vm {
OpCode::SET_PROP_BATCH => self.dispatch_set_prop_batch(rd, a, b),
OpCode::GET_PROP_DYNAMIC => self.dispatch_get_prop_dynamic(rd, a, b),
OpCode::SET_PROP_DYNAMIC => self.dispatch_set_prop_dynamic(rd, a, b),
OpCode::SET_PROP_DYNAMIC_STRICT => self.dispatch_set_prop_dynamic_strict(rd, a, b),
OpCode::SET_ELEM => self.dispatch_set_elem(rd, a, b),
OpCode::GET_PRIVATE => self.dispatch_get_private(rd, a, b),
OpCode::SET_PRIVATE => self.dispatch_set_private(rd, a, b),
@@ -648,8 +649,24 @@ impl Vm {
fn dispatch_get_prop_dynamic(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("GET_PROP_DYNAMIC rd={} a={} b={}", rd, a, b);
let prop_name_si = self.property_key_si(self.regs[a])?;
let receiver = self.regs[rd];
// 基 ToObject 先于键 ToPropertyKey(规范 GetV 步序):null/undefined 基在
// 对象键 toString 副作用之前抛 TypeError。
if receiver.is_null() || receiver.is_undefined() {
self.raise_type_error("GET_PROP_DYNAMIC on non-object")?;
return Ok(());
}
let pc_before = self.pc;
let key_val = self.derive_property_key_value(self.regs[a])?;
if self.pc != pc_before {
// 键推导在途抛错已展开到 catch/finally 入口:pc 由处理器接管,
// 余下读路径不得续行(结果寄存器不得再写)。
return Ok(());
}
// 推导后的键写回键寄存器:复合/逻辑赋值的写侧(SET_PROP_DYNAMIC)
// 复用同一键,对象键的 ToPrimitive 副作用只触发一次。
self.regs[a] = key_val;
let prop_name_si = self.property_key_si(key_val)?;
// 数组元素 fast path:整数键 + 真数组 + 无元素 meta(无 hole/accessor/描述符)
// + 界内 → 直读元素区,免 primitive_property_get / checked_object_ptr /
// ordinary_get 调用链。判定条件与 ordinary_get_inner 数组分支严格等价。
@@ -695,7 +712,13 @@ impl Vm {
let Some(obj_ptr) = self.set_prop_base(self.regs[rd])? else {
return Ok(());
};
let pc_before = self.pc;
let prop_name_si = self.property_key_si(self.regs[a])?;
if self.pc != pc_before {
// 键推导在途抛错已展开到 catch/finally 入口:pc 由处理器接管,
// 余下写路径不得续行(不得写属性)。
return Ok(());
}
// 数组元素 fast path:整数键 + 真数组 + 无 meta + 界内 → 直写元素区,
// 免 __proto__ 查询 / ordinary_set_dispatch / get_own_property_slot 调用链。
// 与 ordinary_set_inner 数组分支等价(界内写不触发 length 扩展、无 shadow
@@ -731,6 +754,52 @@ impl Vm {
Ok(())
}
/// 严格 PutValue 属性写(with 对象引用写回):rd = 基对象、a = 键、b = 值。
/// 写时点 HasProperty 检查(含原型链)——属性缺失抛 ReferenceError(读侧
/// getter 副作用可能已删除属性);存在时按 SET_PROP_DYNAMIC 同形 Set。
///
/// # 步骤
/// 1. 基对象检查(非对象抛 TypeError)。
/// 2. 键推导(ToPropertyKey,在途抛错按 pc 变化停行)。
/// 3. HasProperty 检查(在途抛错按 pc 变化停行);缺失抛 ReferenceError。
/// 4. 存在走 ordinary_set_dispatch(strict 模式)。
fn dispatch_set_prop_dynamic_strict(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("SET_PROP_DYNAMIC_STRICT rd={} a={} b={}", rd, a, b);
let Some(obj_ptr) = self.set_prop_base(self.regs[rd])? else {
return Ok(());
};
let pc_before = self.pc;
let prop_name_si = self.property_key_si(self.regs[a])?;
if self.pc != pc_before {
// 键推导在途抛错已展开到 catch/finally 入口:pc 由处理器接管,
// 余下写路径不得续行(不得写属性)。
return Ok(());
}
let obj = unsafe { &*obj_ptr };
let pc_before = self.pc;
let exists = self.has_property(obj, prop_name_si)?;
if self.pc != pc_before {
// HasProperty 在途抛错已展开:pc 由处理器接管,不得续行。
return Ok(());
}
if !exists {
// 错误消息携带属性名(与全局未声明写同形);键推导后为字符串/符号,
// 非字符串按空名兜底(emit 侧保证字符串键)。
let name = if self.regs[a].is_string() {
// SAFETY: 键推导后为字符串值;错误消息走 lossy 文本。
unsafe { (*self.regs[a].as_string_ptr()).as_lossy_str() }.to_string()
} else {
String::new()
};
self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?;
return Ok(());
}
let obj = unsafe { &mut *obj_ptr };
let value = self.regs[b];
self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd], self.current_strict())?;
Ok(())
}
fn dispatch_set_elem(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("SET_ELEM rd={} a={} b={}", rd, a, b);
let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "Cannot create property on non-object")? else {
+7
View File
@@ -571,6 +571,12 @@ impl Vm {
self.dispatch_define_global_prop_c(a, key_idx)?;
}
OpCode::DEFINE_GLOBAL_PROP_C_STRICT => {
let key_idx = self.bytecode[self.pc] as u16;
self.pc += 1;
self.dispatch_define_global_prop_c_strict(a, key_idx)?;
}
OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT => {
self.dispatch_define_global_prop_if_absent(rd, a, b)?;
}
@@ -642,6 +648,7 @@ impl Vm {
| OpCode::SET_PROP_BATCH
| OpCode::GET_PROP_DYNAMIC
| OpCode::SET_PROP_DYNAMIC
| OpCode::SET_PROP_DYNAMIC_STRICT
| OpCode::SET_ELEM
| OpCode::GET_PRIVATE
| OpCode::SET_PRIVATE
+42
View File
@@ -161,6 +161,48 @@ impl Vm {
///
/// 数组元素区的 hole(删除标记)视同不存在;原型链查找止于
/// `MAX_PROTO_CHAIN_DEPTH` 深度上限。
/// 解析属性的访问器 getter(与 `resolve_property` 同形):沿 shape 链 +
/// 原型链,命中属性为访问器且 get 已定义时返回 getter 函数;数据属性或
/// 无 getter 返回 None。代理层保守返 None(与 `resolve_property` 同口径)。
pub(crate) fn resolve_property_getter(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue> {
if obj.is_proxy_obj() {
return None;
}
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
let val = obj.get_prop_at(pos);
if !val.is_undefined() || obj.prop_vec_len() > pos as usize {
return obj
.prop_meta_at(pos)
.filter(|m| m.is_accessor && !m.get.is_undefined())
.map(|m| m.get);
}
}
let mut proto = obj.proto();
let mut depth = 0usize;
while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH {
depth += 1;
let proto_obj = unsafe { &*proto.as_js_object_ptr() };
if proto_obj.is_proxy_obj() {
return None;
}
if let Some(pos) = self
.kernel_core
.shape_forge()
.lookup_position(proto_obj.shape_id(), prop_name_si)
{
let val = proto_obj.get_prop_at(pos);
if !val.is_undefined() || proto_obj.prop_vec_len() > pos as usize {
return proto_obj
.prop_meta_at(pos)
.filter(|m| m.is_accessor && !m.get.is_undefined())
.map(|m| m.get);
}
}
proto = proto_obj.proto();
}
None
}
pub(crate) fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue> {
vm_trace!("resolve_property: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si);
// Proxy:本方法是不可变签名,无法同步调 get trap;代理无自身属性,
+47
View File
@@ -424,6 +424,53 @@ impl Vm {
return Err(format!("DEFINE_GLOBAL_PROP_C constant index {idx} is not a string key"));
}
let si = self.property_key_si(key_val)?;
self.define_global_prop_c_body(a, si, key_val)
}
/// 严格模式隐式全局写:先 HasProperty 检查(全局环境记录 SetMutableBinding
/// 语义)——属性缺失抛 ReferenceError(未解析引用不可 put);存在时按
/// [`dispatch_define_global_prop_c`] 同形定义。
///
/// # 步骤
/// 1. 键常量解析为属性键 si(与 DEFINE_GLOBAL_PROP_C 同形)。
/// 2. HasProperty 检查(含原型链;代理/deferred 命名空间触发可抛错,
/// 抛错经 pc 变化停行)。
/// 3. 缺失抛 ReferenceError(`{name} is not defined`);存在走定义体。
///
/// # 边界与前提
/// - 键常量必须是字符串(emit 侧保证);非字符串按错误返回。
pub(crate) fn dispatch_define_global_prop_c_strict(&mut self, a: usize, key_idx: u16) -> Result<(), String> {
let idx = key_idx as usize;
vm_trace!("DEFINE_GLOBAL_PROP_C_STRICT value={} idx={}", a, idx);
let key_val = self.immutables().get(idx).copied().unwrap_or(JsValue::undefined());
if !key_val.is_string() {
return Err(format!("DEFINE_GLOBAL_PROP_C_STRICT constant index {idx} is not a string key"));
}
let si = self.property_key_si(key_val)?;
let global_ptr = self.realm.session.borrow().global_object().as_ptr() as *mut JsObject;
// SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。
let obj = unsafe { &*global_ptr };
let pc_before = self.pc;
let exists = self.has_property(obj, si)?;
if self.pc != pc_before {
// HasProperty 在途抛错已展开到 catch/finally 入口:pc 由处理器接管,
// 定义不得续行。
return Ok(());
}
if !exists {
// SAFETY: key_val 已校验为字符串值;错误消息走 lossy 文本。
let name = unsafe { (*key_val.as_string_ptr()).as_lossy_str() };
self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?;
return Ok(());
}
self.define_global_prop_c_body(a, si, key_val)
}
/// 全局对象属性定义体(DEFINE_GLOBAL_PROP_C 与严格变体共用):既有属性
/// 按描述符分流(accessor 走 Set 语义、数据属性按可写性),缺失新建可写/
/// 可枚举/可配置属性。
fn define_global_prop_c_body(&mut self, a: usize, si: u32, key_val: JsValue) -> Result<(), String> {
let _ = key_val;
let value = self.regs[a];
let global_ptr = self.realm.session.borrow().global_object().as_ptr() as *mut JsObject;
// SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。