feat: 落地 Proxy trap 派发框架与属性守卫

This commit is contained in:
2026-10-17 06:36:53 +00:00
parent 9ab71d0585
commit addaab394d
5 changed files with 331 additions and 24 deletions
Generated
+21 -21
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "oxide_builtins"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"chrono",
"fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]]
name = "oxide_bytecode"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"num-bigint",
"oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "oxide_cfg"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "oxide_cli"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"ansi_term",
"bumpalo",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]]
name = "oxide_code_cache"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"lru",
"oxide_bytecode",
@@ -1237,7 +1237,7 @@ dependencies = [
[[package]]
name = "oxide_compiler"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1258,7 +1258,7 @@ dependencies = [
[[package]]
name = "oxide_dce"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1272,7 +1272,7 @@ dependencies = [
[[package]]
name = "oxide_emit"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"num-bigint",
"oxc_allocator",
@@ -1288,7 +1288,7 @@ dependencies = [
[[package]]
name = "oxide_engine"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1300,7 +1300,7 @@ dependencies = [
[[package]]
name = "oxide_fold"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"num-bigint",
"oxide_bytecode",
@@ -1312,7 +1312,7 @@ dependencies = [
[[package]]
name = "oxide_ir"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_log",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]]
name = "oxide_kernel"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"bumpalo",
"dashmap",
@@ -1335,7 +1335,7 @@ dependencies = [
[[package]]
name = "oxide_liveness"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1347,7 +1347,7 @@ dependencies = [
[[package]]
name = "oxide_log"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"tracing",
"tracing-appender",
@@ -1356,11 +1356,11 @@ dependencies = [
[[package]]
name = "oxide_net"
version = "0.20.5"
version = "0.20.6"
[[package]]
name = "oxide_parser"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -1375,7 +1375,7 @@ dependencies = [
[[package]]
name = "oxide_regalloc"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1391,7 +1391,7 @@ dependencies = [
[[package]]
name = "oxide_runtime_api"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"num-bigint",
"num-traits",
@@ -1404,7 +1404,7 @@ dependencies = [
[[package]]
name = "oxide_test262"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1424,7 +1424,7 @@ dependencies = [
[[package]]
name = "oxide_types"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"bumpalo",
"dashmap",
@@ -1437,7 +1437,7 @@ dependencies = [
[[package]]
name = "oxide_vm"
version = "0.20.5"
version = "0.20.6"
dependencies = [
"bumpalo",
"chrono",
+1 -1
View File
@@ -25,7 +25,7 @@ members = [
]
[workspace.package]
version = "0.20.5"
version = "0.20.6"
edition = "2021"
license = "MIT"
rust-version = "1.80"
+228 -2
View File
@@ -3,11 +3,18 @@
//! 状态盒本体(`ProxyState`)定义在 `oxide_builtins::proxy`(构造器侧分配),
//! 本模块只读其布局做 GC 接线;撤销函数(独立 tag)的边函数在
//! `oxide_builtins::proxy::proxy_revoker_native_edges`。
//!
//! trap 派发框架:`call_trap` 从 handler 读 trap 值并同步调用,各入口
//! (get / set / has / defineProperty)的 is_proxy 守卫路由到本助手;逐 trap
//! 完整语义(不变量检查、receiver 语义、target 自身属性描述符)由后续卡补全。
use oxide_builtins::proxy::ProxyState;
use oxide_types::object::JsObject;
use oxide_runtime_api::VmHost;
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::value::JsValue;
use crate::vm::Vm;
/// Proxy 状态盒内全部值边(GC mark 边):target 与 handler。
pub(crate) fn proxy_native_edges(obj: &JsObject) -> Vec<JsValue> {
let Some(state) = oxide_builtins::proxy::proxy_state_ref(obj) else {
@@ -42,10 +49,90 @@ pub(crate) fn drop_proxy_native(obj: &JsObject) -> u64 {
bytes
}
/// trap 调用助手:从 handler 读 trap 值并同步调用。
///
/// # 步骤
/// 1. 取状态盒并检 revoked 位:位真抛 TypeError("Proxy is revoked")。
/// 2. 经全 get 路径从 handler 读 trap 值(receiver 为 handler——handler 自身
/// 是代理时其 get trap 依规范触发)。
/// 3. trap 值 undefined 返 None(落穿普通行为)。
/// 4. 经 `call_function_sync` 同步调用 trap 值(this 为 handler)。
///
/// # 边界与前提
/// - `proxy` 须为带状态盒的 Proxy 对象(守卫以 `is_proxy_obj` 保证入口)。
/// - 代理对象须为 GC 根(活栈上),handler 与 target 经家族表边标活,
/// trap 调用期间状态盒不被回收(重入 GC 安全点)。
///
/// # 副作用
/// - trap 调用执行用户代码(含分配与 GC);trap 返回值原样透传。
///
/// # 注意事项
/// - 逐 trap 返回值校验(布尔投影、不变量)由各卡守卫补全,本助手只给通用形态。
pub(crate) fn call_trap(
vm: &mut Vm, proxy: &JsObject, trap_name_si: u32, args: &[JsValue],
) -> Result<Option<JsValue>, String> {
let state = oxide_builtins::proxy::proxy_state_ref(proxy).expect("proxy state box must exist");
if state.revoked {
return Err(vm.error_message_text("TypeError", "Proxy is revoked"));
}
let handler = state.handler;
// SAFETY: handler 由构造器校验为非空对象指针。
let handler_obj = unsafe { &*handler.as_js_object_ptr() };
let trap_val = vm.ordinary_get(handler_obj, trap_name_si, handler)?;
if trap_val.is_undefined() {
return Ok(None);
}
let result = vm.call_function_sync(trap_val, handler, args)?;
Ok(Some(result))
}
/// 布尔 trap 返回值校验:trap 返回值投影为布尔,非布尔返回抛 TypeError。
///
/// 错误文本为 `TypeError: ...` 前缀形态,调用边界经 `create_from_text`
/// 恢复为 TypeError 异常对象。
pub(crate) fn trap_bool_result(result: JsValue, trap_name: &str) -> Result<bool, String> {
if result.is_bool() {
return Ok(result.as_bool());
}
Err(oxide_runtime_api::format_error_message(
"TypeError",
&format!("Proxy {trap_name} trap must return a boolean"),
))
}
/// 构造 defineProperty trap 的属性描述符对象(value 加三标志位)。
///
/// 描述符属性均为不可写、不可枚举、不可配置(与内建对象属性口径一致);
/// 对象原型链落 Object.prototype。
pub(crate) fn define_property_descriptor(vm: &mut Vm, val: JsValue, attributes: PropAttributes) -> JsValue {
let proto = vm.builtin_proto(oxide_runtime_api::ProtoKind::ObjectProto);
let obj = vm.alloc_object(JsObject::new_empty(
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
JsValue::from_js_object(proto),
));
let attrs = PropAttributes::new(false, false, true);
let obj_ref = unsafe { &mut *obj };
let _ = vm.define_data_property(obj_ref, vm.perm_intern("value"), val, attrs);
let _ = vm.define_data_property(obj_ref, vm.perm_intern("writable"), JsValue::bool(attributes.writable()), attrs);
let _ =
vm.define_data_property(obj_ref, vm.perm_intern("enumerable"), JsValue::bool(attributes.enumerable()), attrs);
let _ = vm.define_data_property(
obj_ref,
vm.perm_intern("configurable"),
JsValue::bool(attributes.configurable()),
attrs,
);
JsValue::from_js_object(obj)
}
#[cfg(test)]
mod tests {
use super::*;
use oxide_types::shape::EMPTY_SHAPE_ID;
use crate::native::NativeFn;
use crate::vm::Vm;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{NativeResult, ProtoKind};
use oxide_types::object::NativeFnPtr;
/// 建一个带状态盒的 Proxy 对象(测试构造路径,不经 Vm 分配)。
fn proxy_obj(target: JsValue, handler: JsValue) -> JsObject {
@@ -66,6 +153,145 @@ mod tests {
JsValue::from_js_object(Box::into_raw(obj))
}
/// get trap 原生函数体:逼一次完整收集并返回固定值。
fn gc_forcing_get_trap(vm: &mut Vm, _args: &[u8]) -> NativeResult {
vm.collect_session_gc();
NativeResult::Ok(JsValue::int(42))
}
/// 计数 trap 原生函数体:receiver(handler)的计数属性加一,返回 true。
fn counting_trap(vm: &mut Vm, _args: &[u8]) -> NativeResult {
let receiver = vm.reg(253);
// SAFETY: receiver 由 trap 调用约定保证为 handler 对象值。
let handler = unsafe { &mut *receiver.as_js_object_ptr() };
let hits_si = vm.perm_intern("hits");
let cur = vm.resolve_property(handler, hits_si).unwrap_or(JsValue::int(0));
vm.set_or_create_prop_value(handler, hits_si, JsValue::int(cur.as_int() + 1));
NativeResult::Ok(JsValue::bool(true))
}
/// 建带原生函数项的函数对象(session 堆分配并登记)。
fn native_fn_obj(vm: &mut Vm, func: NativeFn) -> *mut JsObject {
let proto = vm.builtin_proto(ProtoKind::FunctionProto);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto));
obj.set_function(true);
// SAFETY: func 是合法 NativeFn 函数项。
obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(func as *const ()) }));
obj.set_native_arg_count(0);
vm.alloc_object(obj)
}
/// 建带状态盒的 Proxy 对象(target、handler、代理均登记 session 堆)。
fn proxy_with_state(vm: &mut Vm, target: JsValue, handler: JsValue) -> *mut JsObject {
let proto = vm.builtin_proto(ProtoKind::ObjectProto);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto));
obj.type_tag = JsObject::OBJ_TYPE_PROXY;
let state = Box::new(ProxyState {
target,
handler,
revoked: false,
});
obj.set_native_data(Box::into_raw(state) as *mut u8);
vm.alloc_object(obj)
}
/// 重入 GC 安全点:trap 调用期间逼完整收集,handler 与 target 指针身份
/// 不变(经状态盒边标活),且收集计数推进(收集确在 trap 调用期间发生)。
#[test]
fn trap_call_gc_safety_point_keeps_handler_and_target_alive() {
let mut vm = Vm::new();
let target = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()));
let handler = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()));
let trap_fn = native_fn_obj(&mut vm, gc_forcing_get_trap);
let get_si = vm.perm_intern("get");
vm.set_or_create_prop_value(unsafe { &mut *handler }, get_si, JsValue::from_js_object(trap_fn));
let proxy = proxy_with_state(&mut vm, JsValue::from_js_object(target), JsValue::from_js_object(handler));
// 代理置于活栈寄存器(Regs 根组,避开实参打包区),与生产 trap 调用路径同形。
vm.regs[4] = JsValue::from_js_object(proxy);
let before = vm.session_gc_stats().total_collections;
let key_si = vm.perm_intern("k");
let result = vm
.ordinary_get(unsafe { &*proxy }, key_si, JsValue::from_js_object(proxy))
.expect("get 应成功");
assert_eq!(result, JsValue::int(42), "get trap 返回值须原样返回");
assert!(vm.session_gc_stats().total_collections > before, "trap 调用期间应发生收集");
let state = oxide_builtins::proxy::proxy_state_ref(unsafe { &*proxy }).expect("状态盒");
assert!(
std::ptr::eq(state.target.as_js_object_ptr(), target),
"target 不得被 trap 调用期间的收集回收"
);
assert!(
std::ptr::eq(state.handler.as_js_object_ptr(), handler),
"handler 不得被 trap 调用期间的收集回收"
);
}
/// 已撤销代理:全部 trap 派发抛 TypeError。
#[test]
fn call_trap_revoked_throws_type_error() {
let mut vm = Vm::new();
let target = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()));
let handler = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()));
let proxy = proxy_with_state(&mut vm, JsValue::from_js_object(target), JsValue::from_js_object(handler));
{
let state = oxide_builtins::proxy::proxy_state_mut(unsafe { &mut *proxy }).expect("状态盒");
state.revoked = true;
}
let get_si = vm.perm_intern("get");
let err = call_trap(&mut vm, unsafe { &*proxy }, get_si, &[]).expect_err("已撤销代理须抛错");
assert!(err.contains("TypeError"), "已撤销代理须抛 TypeError:{err}");
assert!(err.contains("Proxy is revoked"), "错误文本须带撤销标记:{err}");
}
/// 守卫路由:get / set / has trap 对代理命中,resolve_property 对代理保守返 None。
#[test]
fn guard_routing_for_proxy_get_set_has_and_resolve() {
let mut vm = Vm::new();
let target = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()));
let handler = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()));
let trap_fn = native_fn_obj(&mut vm, counting_trap);
let handler_val = JsValue::from_js_object(handler);
for name in ["get", "set", "has"] {
vm.set_or_create_prop_value(
unsafe { &mut *handler },
vm.perm_intern(name),
JsValue::from_js_object(trap_fn),
);
}
let proxy = proxy_with_state(&mut vm, JsValue::from_js_object(target), handler_val);
let key_si = vm.perm_intern("k");
let hits_si = vm.perm_intern("hits");
let result = vm.ordinary_get(unsafe { &*proxy }, key_si, handler_val).expect("get 应成功");
assert_eq!(result, JsValue::bool(true), "get trap 返回值须原样返回");
assert_eq!(
vm.resolve_property(unsafe { &*handler }, hits_si).unwrap(),
JsValue::int(1),
"get trap 须命中"
);
vm.ordinary_set(unsafe { &mut *proxy }, key_si, JsValue::int(7), handler_val, true)
.expect("set 应成功");
assert_eq!(
vm.resolve_property(unsafe { &*handler }, hits_si).unwrap(),
JsValue::int(2),
"set trap 须命中"
);
let exists = vm.has_property(unsafe { &*proxy }, key_si).expect("has 应成功");
assert!(exists, "has trap 须返回 true");
assert_eq!(
vm.resolve_property(unsafe { &*handler }, hits_si).unwrap(),
JsValue::int(3),
"has trap 须命中"
);
assert!(
vm.resolve_property(unsafe { &*proxy }, key_si).is_none(),
"resolve_property 对代理须保守返 None"
);
}
#[test]
fn proxy_native_edges_return_target_and_handler() {
let target = plain_obj_value();
+22
View File
@@ -163,6 +163,11 @@ impl Vm {
/// `MAX_PROTO_CHAIN_DEPTH` 深度上限。
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;代理无自身属性,
// 保守返 None(get trap 是权威,须由可变签名入口承载)。
if obj.is_proxy_obj() {
return None;
}
let length_si = self.length_si;
if obj.is_array() && prop_name_si == length_si {
return Some(obj.logical_len_value());
@@ -218,6 +223,23 @@ impl Vm {
/// (`in` 运算符逐级走 HasProperty;非 deferred 对象触发为 no-op),触发失败
/// (cyclic 守卫抛 TypeError / 失败缓存原值重抛)走可捕获异常展开。
pub(crate) fn has_property(&mut self, obj: &JsObject, prop_name_si: u32) -> Result<bool, String> {
// Proxy [[HasProperty]]:has trap 以(target、P)调用;trap 报 true 即
// 存在;报 false 落穿 target 的普通存在性判定(不变量检查由后续卡补全)。
if obj.is_proxy_obj() {
let target = oxide_builtins::proxy::proxy_state_ref(obj).expect("状态盒").target;
let key = oxide_builtins::object::key_si_to_js_value(self, prop_name_si);
if let Some(r) = crate::proxy::call_trap(self, obj, self.perm_intern("has"), &[target, key])? {
if crate::proxy::trap_bool_result(r, "has")? {
return Ok(true);
}
// target 即本代理(自环)时保守判不存在,避免无限递归。
if std::ptr::eq(target.as_js_object_ptr(), obj as *const JsObject) {
return Ok(false);
}
// SAFETY: target 由构造器校验为非空对象指针。
return self.has_property(unsafe { &*target.as_js_object_ptr() }, prop_name_si);
}
}
let length_si = self.length_si;
let mut current = Some(obj);
let mut depth = 0usize;
+59
View File
@@ -108,6 +108,15 @@ impl Vm {
&mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option<u8>,
) -> Result<JsValue, String> {
vm_trace!("ordinary_get_inner: shape={} prop_si={}", obj.shape_id(), prop_name_si);
// Proxy [[Get]]:get trap 权威(target、P、Receiver),trap 定义时其返回值
// 即读结果;trap 未定义落穿普通属性路径。
if obj.is_proxy_obj() {
let target = oxide_builtins::proxy::proxy_state_ref(obj).expect("状态盒").target;
let key = oxide_builtins::object::key_si_to_js_value(self, prop_name_si);
if let Some(v) = crate::proxy::call_trap(self, obj, self.perm_intern("get"), &[target, key, receiver])? {
return Ok(v);
}
}
let length_si = self.length_si;
let mut current = Some(obj);
let mut depth = 0usize;
@@ -425,6 +434,30 @@ impl Vm {
prop_name_si,
use_frame_push
);
// Proxy [[Set]]:set trap 以(target、P、Value、Receiver)调用;trap 报
// true 写成功;报 false 落穿 target 的普通写路径(不变量检查由后续卡补全)。
if obj.is_proxy_obj() {
let target = oxide_builtins::proxy::proxy_state_ref(obj).expect("状态盒").target;
let key = oxide_builtins::object::key_si_to_js_value(self, prop_name_si);
if let Some(r) = crate::proxy::call_trap(self, obj, self.perm_intern("set"), &[target, key, val, receiver])?
{
if crate::proxy::trap_bool_result(r, "set")? {
return Ok(());
}
// target 即本代理(自环)时按写失败处理,避免无限递归。
if std::ptr::eq(target.as_js_object_ptr(), obj as *const JsObject) {
return self.write_protection_failure(builtin, "Proxy set trap reported false");
}
// SAFETY: target 由构造器校验为非空对象指针。
return self.ordinary_set_inner(
unsafe { &mut *target.as_js_object_ptr() },
prop_name_si,
val,
receiver,
contract,
);
}
}
// 模块命名空间 exotic [[Set]] 恒 false(规范 10.4.6.8,deferred 形态同口径
// 不触发求值):严格抛 TypeError,sloppy 静默 no-op;Reflect.set 以
// strict=true 调用并投影为 false。
@@ -1094,6 +1127,32 @@ impl Vm {
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes,
) -> Result<(), String> {
vm_trace!("define_data_property: shape={} prop_si={}", obj.shape_id(), prop_name_si);
// Proxy [[DefineOwnProperty]]:defineProperty trap 以(target、P、Desc)
// 调用;trap 报 true 定义成功;报 false 落穿 target 的普通定义路径
// (不变量检查由后续卡补全)。
if obj.is_proxy_obj() {
let target = oxide_builtins::proxy::proxy_state_ref(obj).expect("状态盒").target;
let key = oxide_builtins::object::key_si_to_js_value(self, prop_name_si);
let desc = crate::proxy::define_property_descriptor(self, val, attributes);
if let Some(r) =
crate::proxy::call_trap(self, obj, self.perm_intern("defineProperty"), &[target, key, desc])?
{
if crate::proxy::trap_bool_result(r, "defineProperty")? {
return Ok(());
}
// target 即本代理(自环)时按定义失败处理,避免无限递归。
if std::ptr::eq(target.as_js_object_ptr(), obj as *const JsObject) {
return Err("Proxy defineProperty trap reported false".to_string());
}
// SAFETY: target 由构造器校验为非空对象指针。
return self.define_data_property(
unsafe { &mut *target.as_js_object_ptr() },
prop_name_si,
val,
attributes,
);
}
}
// TypedArray 统一数值键门:界内规范键走元素定义(界内写 buffer);
// 规范越界键(含 detach:live 长 0)拒绝定义;非规范数值串
// ("+1"/"1.0" 等,round-trip 不成)与 symbol 键落命名属性路径。