From c1dfce31b7297bfa6580838af53c36a735d8f3f3 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 14:08:37 +0800 Subject: [PATCH 01/14] docs: plan ordinary property access kernel --- docs/ordinary-property-plan.md | 197 +++++++++++++++++++++++++++++++++ src/engine/object/README.md | 3 + 2 files changed, 200 insertions(+) create mode 100644 docs/ordinary-property-plan.md diff --git a/docs/ordinary-property-plan.md b/docs/ordinary-property-plan.md new file mode 100644 index 00000000..234cf7e8 --- /dev/null +++ b/docs/ordinary-property-plan.md @@ -0,0 +1,197 @@ +# 普通对象属性访问内核改造计划 + +状态:设计草案,尚未实现、尚未测量。源码核对基线 `fec7519`(2026-09-11)。 +面向实施者和评审者:按本文迁移普通属性读、写、定义及元数据查询,并验证语义、所有权和实际性能。 +来源为 benchmark issue #16 的对象属性问题;历史热点仅用于选方向,不能作为当前收益预测。 + +## 1. 目标与范围 + +把普通属性操作组织成三层:存储定位与事务、普通属性算法、可观察的内部方法分派。 +现有文件划分不是约束,可以移动、合并和删除旧实现;对象/heap/VM 的具体接口也可以调整。 +需要保留的是语言行为、公开 API 契约、运行时域隔离、引用所有权及安全 Rust,而不是旧调用链。 + +本轮交付包括: + +- 自有普通数据属性写入不预扫原型链,不为旧值生成完整描述符,不重复走完整 Define 算法。 +- 普通 Get/Get-or-missing 按需取得数据值或 getter;Has、枚举筛选共享元数据定位。 +- value-only Define 复用已有槽事务;完整 Define 和描述符查询有各自明确入口。 +- 普通原型链按需逐层访问,命中即停;移除“先证明全链普通,再重新执行”的路线。 +- 迁移后删除被替代的普通读写实现,避免永久保留两套 Set 规则。 + +首批直接优化对象为明确的 `ObjectPayload::Ordinary`,物理存储同时覆盖 shared shape 和 dictionary。 +首批槽覆盖 Data/Accessor;VarRef/AutoInit 作为显式特殊状态处理,不能误当普通数据槽。 +其他 payload 即使有普通命名属性,也必须逐类证明后才能复用;不以“不是 Proxy”推导普通。 +Array、TypedArray、Arguments、String wrapper、模块命名空间和全局对象的语义入口保留并参与回归。 +本轮不包含数值运算、跨操作 inline cache、shape 布局重写、批量冻结/删除或对象分配池。 + +## 2. 当前代码证据 + +这是本次决策的源码索引,实施时以符号及调用者为准: + +| 现有位置 | 事实 | 需要改变的工作 | +| --- | --- | --- | +| `object/internal_methods.rs::ordinary_set_fast_path_available` | 检查 receiver 后遍历完整 prototype 链,排除 Proxy/TypedArray | 自有属性命中也付出全链分类成本;改为访问到哪层才分类哪层 | +| `internal_set` 与 `properties.rs::prepare_set_property_with_receiver_in_realm` | 同时存在完整通用 Set 路线和预扫后的普通路线 | 收敛普通语义,不新增第三套永久路线 | +| `prepare_set_property_with_receiver_in_realm` | 查询 target,然后再次查询 receiver,再调用 Define | receiver==target 且已有 Data 时复用本次定位 | +| `properties.rs::get_own_property` | shape/slot 快照后生成完整 rooted descriptor;accessor 同时 root get/set | 根据操作只提取需要的值、可调用对象或 flags | +| `internal_get` / `internal_get_or_missing` | 经完整 GetOwnProperty 获取读结果,多层重复对象分类 | 共用普通查找步骤,保留 missing 与 undefined 区分 | +| `define_ordinary_own_property`、`operations.rs` | current descriptor 和验证记录之间转换并 clone Value | value-only 定义避免无关字段往返;其余定义保留单一验证算法 | +| `storage.rs::store_property_slot` | 已经可以同 flags 单槽替换,但再次按 key 查槽 | 让合法的本次定位在事务内部直接用于替换 | +| `internal_has_own_property` / 两种 enumerable 查询 | 已有部分 metadata 路线;普通 recheck 和枚举快照的 AutoInit 行为不同 | 复用定位,但不可合并可观察语义 | +| `heap/object_storage.rs::replace_object_slot` | 验证新槽、保留新边、替换、释放旧边、drain,返回 cleanup | 作为事务基础,不绕过引用预检 | + +完整描述符是 Rust record,不能把它的每次构造都说成一次堆分配。实际成本包括查找、RawValue/Value +转换及 clone、对象/Atom retain-release、Runtime 借用和重复分类。具体占比需要当前基线采样。 +`prepare_get_property_with_receiver_or_missing` 当前是测试专用入口,不应把它当生产 Get 的热点。 + +## 3. 建议的模块和接口 + +以下是目标组织,可因 Rust 借用实现细化;不要先大规模搬文件再寻找使用者。 + +### 3.1 存储层:一次定位、按需提取、原子替换 + +建议增加 `object/ordinary_storage.rs`,配合 `heap/object_storage.rs` 和 RuntimeState 的 Atom/cleanup 管理。 +该模块不接收 realm,不调用 VM、Proxy trap、getter、setter 或用户转换。 + +私有定位结果表达:Missing,或 slot index + flags + slot kind。位置不是新的持久句柄。 +通过持有 `&mut RuntimeState` 的短生命周期具体 session,或单个封闭方法,完成定位和消费。 +优先采用具体类型和方法,不引入操作模式布尔集合、策略 trait 或通用闭包执行框架。 + +建议的操作职责(名称为设计草案,不是已存在 API): + +- `lookup_own(atom)`:在本次 session 内定位,返回内部视图;不生成 rooted descriptor。 +- `read_data` / `root_getter` / `root_setter`:只保留将跨出借用范围的那个值。 +- `replace_data`:消费本次定位,在同一独占访问中更新同 flags Data 槽。 +- `materialize_descriptor`:完整描述符查询才读取并 root 全部所需字段。 + +slot index 和视图不得导出到公开/VM API,不实现可随意复制的“已验证 slot token”。 +不能只靠 shape id 判断跨操作位置有效:dictionary 可以原地修改,swap-remove 可以移动槽。 +定位后到提交前禁止布局变更;提交后视图立即失效,不能在 cleanup 后继续使用。 + +**root 生成必须解决借用问题。** 当前 `Runtime::root_raw_value` 会再次借用 Runtime;不能从 +持有 RuntimeState 可变借用的 session 内直接调用它。增加或复用 state 内的 retain/promotion +原语,在同一状态访问中保留必要边,释放借用后用已有 owned-handle 构造器组装 Value。 +跨层转移的是明确已拥有的引用,组装失败必须有回滚;不把裸 RawValue 误当 root。 +另一种实现可以在禁止回调和修改的短读区间快照,然后立即 root,但必须证明旧边在 root 前仍存活, +且不得因此让写事务退化为导出裸 slot。P1 中用实际编译与所有权测试选择实现,不改变此契约。 + +**写事务顺序:**先验证 runtime 域及输入,准备 RawValue,保留新 Atom/heap edges,再发布新槽, +随后释放旧边并处理 cleanup。保留现有 retain 失败不修改对象的保证。 +提交后的 cleanup/invariant 错误不冒充“完全未发生写入”;要明确区分发布前和发布后错误。 +不得在持有 state 借用时随意 drop rooted ObjectRef/Value,避免隐式重入借用或改变延迟释放协议。 +旧值与新值相同、重复边、符号、对象自引用和最终引用释放均需测试。 + +### 3.2 普通算法层:Set/Get/Define 各自明确,共享存储机制 + +建议 `object/ordinary.rs` 拥有普通算法,在代码量确有需要时再拆 read/set/define 子模块。 +结果用具体枚举表达已完成、拒绝、需要调用、继续原型查找和进入特殊语义;不要把异常当普通 miss。 + +**Set:** + +1. 查询当前对象自己的属性;命中 ordinary Data 且 receiver==target,直接检查 writable 并更新。 +2. 自有只读数据属性直接拒绝;即便新旧 SameValue 也不能接受普通赋值。 +3. 命中 accessor,只 root setter 和所需输入,结束借用后调用;receiver 保持原始值。 +4. 缺失才进入 prototype;全链缺失相当于默认 writable Data 的 Set 行为,转到 receiver 自有属性处理。 +5. receiver 不同,不能把 target 的槽复用于 receiver;receiver 为 Proxy 时必须保留其 GetOwnProperty/Define trap。 +6. 新建属性、特殊 receiver、特殊槽进入所属语义,不从叶级路径重放已经执行的回调。 + +**Get / Get-or-missing:**普通节点共用一个查询步骤;数据只返回 value,accessor 只返回 getter。 +缺失才取原型,优先用迭代推进普通节点;遇到 Proxy 等特殊节点转交可观察入口。 +Proxy Get 是 terminal observable boundary,不能把其 undefined 再解释成 missing;保留全局引用错误行为。 +若调整递归形态,核对现有 Proxy 深度及栈溢出契约,不能意外绕过其预算。 + +**Define:**只针对“仅 value 字段存在、普通对象、已有 Data”使用明确捷径。 +可写属性接受;不可写但 configurable 的属性也可更新;不可配置且不可写的属性只接受 SameValue。 +NaN、+0/-0、对象身份按 SameValue 而非 `==`。该判定放在现有纯描述符验证所属模块, +共享已有规则,并用属性组合测试对照完整验证算法;不能在多个调用者复制条件。 +空 descriptor、属性变更、Data/Accessor 转换、缺失属性和特殊槽走完整算法。 +完整验证可以继续输出完整记录;首轮不为消除所有转换而改写整个 descriptor 类型系统。 + +**Has / enumerable / descriptor:**共用存储定位,语义入口分别命名。 +HasOwn 和 own-key snapshot 可以按现有约定不物化 AutoInit;普通 enumerable recheck / descriptor 查询 +仍按现有约定物化;模块 VarRef 的 TDZ 及 Proxy trap 不可被纯 flags 查询绕过。 + +### 3.3 分派层:对象类别、可观察调用与错误映射 + +`internal_methods` 保留或拆出 Proxy/internal-property 分派文件,负责特殊对象、realm、receiver、 +JavaScript 调用、Throw 和 API 层拒绝映射。不再拥有第二份普通 Set 算法。 +允许移动现有 Array/Arguments 定义代码到所属模块,但只随对应迁移进行,不扩大成目录重排项目。 +分类必须针对操作及 key:TypedArray 的整数索引、Array length、String 虚拟索引不能用一枚全局 +“ordinary=true”覆盖。P1 采用 Ordinary payload 白名单,使首批适用条件可审查。 +PropertyKey 转换和 ToPropertyDescriptor 在外部语义入口完成,内核不能再执行一次转换。 + +## 4. 迁移切片与完成条件 + +所有步骤当前均为未开始。每步提交后单独测量;先跑目标语义,再扩大回归。 + +| 步骤 | 交付与依赖 | 完成条件 | +| --- | --- | --- | +| P0 当前基线与行为矩阵 | 捕获生产 Get/Set/Define/Has 调用者,冻结测试负载和基线 ELF;收集属性相关 profile | 能区分链遍历、descriptor/root 和存储成本;无改动收益声明 | +| P1 自有数据写入贯穿三层 | 建立最小存储 session、共享引用事务、ordinary Set 入口;先在旧预扫之前命中 Ordinary Data 自有写入 | 无原型预扫、无旧值 descriptor、单次定位;shared/dictionary 和引用失败验证通过 | +| P2 普通链与 accessor Set 收敛 | 扩展同一算法处理缺失、继承、异 receiver 和 accessor;特殊对象仍分派 | 删除 `ordinary_set_fast_path_available` 和替代掉的普通 Set 分支;保留一个普通算法和特殊对象算法 | +| P3 Get 与元数据消费者 | Get/Get-or-missing、Has、enumerable、完整 descriptor 接入共同定位与按需提取 | Getter 只 root getter;missing/undefined、AutoInit、Proxy/TDZ 语义矩阵通过 | +| P4 value-only Define | 依赖 P1/P3;纯规则判定接入已定位槽更新 | flags 全组合及 SameValue 对照通过,不把只读 Set 与 Define 混同 | +| P5 收口 | 删除迁移桥接/死实现,更新模块 README 和架构规则;完整回归与最终性能矩阵 | 无第二套普通规则、无裸 slot 跨回调、性能结论包含退化与控制组 | + +P1 允许短暂保留旧路线作为迁移 fallback;P2/P5 必须清理。测试专用入口若仍需要,应委托同一算法, +不能保留只供测试通过的旧内核。现有架构 hash/canary 可随职责变化调整,但必须更新能命中新入口的反例, +不能把旧结构断言当作禁止重构的理由,也不能仅改 hash 放行。 + +删除、新建属性、批量 define/freeze 和原型变更参与控制测试,本轮不承诺对它们加速。 +若 P0 发现某个相邻路径是主要热点,可写明新证据再扩展切片,不能隐式增加实施范围。 + +## 5. 必须覆盖的行为与所有权 + +- 普通 Data:自有/继承/缺失,writable/configurable/enumerable 组合,非 extensible,shared/dictionary,字符串及 Symbol key。 +- Set:严格赋值、非严格赋值、Reflect.set 返回值,异 receiver、primitive receiver、receiver 自有 accessor;错误类型与已冻结诊断保持一致。 +- 原型链:自有命中下方为深链或 revoked Proxy 时不访问下方;缺失遇 Proxy 时 trap 次数、顺序、receiver 正确。 +- Accessor:getter/setter 修改目标布局、删除/重建属性、递归访问、抛错;Get 不触及 setter,Set 不调用 getter。 +- Define:value 字段缺失与显式 undefined,空 descriptor,混合 descriptor 拒绝,NaN、±0、相同/不同对象;所有 flags 组合对照完整验证。 +- 特殊语义:AutoInit 物化/拒绝顺序、模块 TDZ/global VarRef、mapped Arguments 解除映射、Array length/索引、String 虚拟索引、TypedArray detached/resizable 和转换重入。 +- 生命周期:跨 Runtime object/key/value/descriptor 拒绝;重复引用、引用溢出/保留失败、发布前回滚、发布后 cleanup;drop/GC 无泄漏、双释放或借用 panic。 +- 组合操作:`obj.x++`、`obj.x += value` 的转换期间修改布局,确认 Get 与 Set 不共享失效定位。 +- 架构反例:绕过域检查、retain 前发布、错误复用 receiver 槽、把特殊对象纳入白名单、跳过 AutoInit、让 slot 逃离 session、重放 trap。 + +纯判定测试归 `object/property`,新内核契约测试归新模块;低级引用事务测试复用 heap/runtime 测试设施。 +JS 可观察行为用现有 CLI/QuickJS differential 和 Test262;新用例进入现有 registry,不改冻结行为向量。 + +## 6. 测量与回归流程 + +固定工作量,不采用 Date.now 最小窗口成绩衡量小幅收益。每个优化提交与直接前序做普通 release +交错 A/B;机器、CPU、工具链、flags、工作负载哈希和输出固定,构建/测试与 timing 串行。 +首轮至少五轮;疑似退化追加十轮,并报告分布、instructions/cycles 和整进程时间。 + +新增诊断矩阵: + +- 同一自有属性固定写入次数,原型深度 0/1/8/64/256;另有同样 setup、无写循环控制,单独报告不伪装成纯阶段耗时。 +- 对象宽度 4/32/256/2048,同时测试共享 shape 与删除触发的 dictionary;控制变量固定总操作数。 +- 属性值分别为 Int、Float、String、Symbol、对象;区分重复同一值和两个预建值轮换,不把分配成本混入所有场景。 +- Get、自有 Set、继承 Data Set、accessor、异 receiver、value-only Define、HasOwn 和枚举查询分别测量。 +- Proxy、特殊对象、缺失属性为语义与性能控制组;首次写入和 steady repeated writes 分开配置。 + +诊断 instrumentation 只证明工作数量,不用于正式 timing;如能稳定计数,验证自有 Set 的 prototype visits=0、 +旧值 descriptor materializations=0,且定位次数不随链深度增长。不要把源码函数调用次数等同于机器指令成本。 +正式性能判断还包括 `prop_read/write/update/create/delete`、array/arguments/global 控制和完整 50+8 固定矩阵。 +无预设加速倍数;新增复杂度若没有可复现耗时收益,应调整或撤回,不能仅以指令数下降验收。 + +回归执行入口以当前 CI 和测试文档为准,包括: + +```sh +cargo fmt --all -- --check +cargo test --locked --workspace --all-targets +python3 -m unittest discover -s scripts/benchmark -p 'test_*.py' +./scripts/checks/check-binary-object-boundary.sh +PYTHONPATH=scripts/checks python3 -m unittest discover -s scripts/checks/binary_object/tests +python3 scripts/checks/check-source-layout.py +``` + +最终还须执行 CI 固定 Rust 的全部 Clippy/feature 组合、profiling/test262-host/doc、QuickJS differential、 +focused/full Test262、架构变异、Node/WASM 验收。使用当前 CI 的确切命令,不把上述短清单当完整门禁。 +Test262 fingerprint 变化与实际行为变化分别报告,不能改冻结 receipts 让新结果通过。 + +## 7. 交付记录 + +每个切片记录源码提交、修改拥有者、已运行验证、未运行验证、基线/新 ELF 及工作负载哈希、A/B 分布、 +退化和最终处置。原始实验保留在忽略的 target;计划和维护契约入库,不预填结果。 +涉及借用/引用事务及 Set 语义收敛的变更需要独立评审,实施者自查不得称为独立评审。 +本文当前仅完成源码驱动的设计,P0–P5 均未实施。 diff --git a/src/engine/object/README.md b/src/engine/object/README.md index 08804f07..132deee5 100644 --- a/src/engine/object/README.md +++ b/src/engine/object/README.md @@ -4,6 +4,9 @@ 通过 heap 保存原始记录与引用边;调用和用户代码执行交给 VM 协调;内置方法归 builtins。 +普通属性读写与定义的后续重构见[普通对象属性访问内核改造计划](../../../docs/ordinary-property-plan.md)。 +该文档为尚未实施的设计,包含目标职责、迁移步骤和验收条件。 + Dictionary 模式用于普通对象和已转为慢表示的 Array。共享 shape 首次分离,独占 shape 原地转换并脱离 weak cache。物理槽用 swap-remove,插入顺序由独立双向链接维护; `Shape::entries()` 只表示槽顺序,可观察遍历必须使用 `ordered_indices()` 或 From 88a39d16f8230f83e31f9478a89d28032a745845 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 14:12:17 +0800 Subject: [PATCH 02/14] perf(object): replace own data slots without descriptor round trips --- src/engine/object/internal_methods.rs | 9 ++ src/engine/object/mod.rs | 2 + src/engine/object/ordinary_storage.rs | 158 ++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/engine/object/ordinary_storage.rs diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index bb68e5bb..d48faaef 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -1032,6 +1032,15 @@ impl Runtime { value: Value, receiver: Value, ) -> Result, RuntimeError> { + if matches!(&receiver, Value::Object(target) if target == object) + && let Some(accepted) = self.try_set_ordinary_own_data(object, key, &value)? + { + return Ok(NativeConversion::Value(if accepted { + InternalSetResult::Accepted + } else { + InternalSetResult::Rejected(PropertySetRejection::ReadOnly) + })); + } if self.proxy_snapshot_if_any(object)?.is_some() { return self.proxy_set(realm, object, key, value, receiver); } diff --git a/src/engine/object/mod.rs b/src/engine/object/mod.rs index 791b3f02..61847e5a 100644 --- a/src/engine/object/mod.rs +++ b/src/engine/object/mod.rs @@ -748,3 +748,5 @@ pub(crate) mod access; pub(crate) mod storage; pub(crate) mod operations; + +mod ordinary_storage; diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs new file mode 100644 index 00000000..5458f6f9 --- /dev/null +++ b/src/engine/object/ordinary_storage.rs @@ -0,0 +1,158 @@ +//! Short, non-reentrant access to ordinary own slots. Slot positions never +//! leave this module and a write locates and commits under one state borrow. +use crate::engine::api::runtime::Runtime; +use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::Atom; +use crate::engine::heap::runtime::RuntimeState; +use crate::engine::heap::{ObjectId, ObjectPayload, PropertySlot}; +use crate::engine::object::shape::PropertyFlags; +use crate::engine::object::{ObjectRef, PropertyKey}; +use crate::engine::value::Value; + +struct OwnSlot { + index: usize, + flags: PropertyFlags, +} + +// The caller keeps the state borrowed until the located slot is consumed. +fn locate( + state: &RuntimeState, + object: ObjectId, + atom: Atom, +) -> Result, RuntimeError> { + let data = state.heap.object(object)?; + let shape = state.heap.shape(data.shape)?; + let Some(index) = shape.find(atom) else { + return Ok(None); + }; + let index = index as usize; + let entry = shape.entries().get(index).ok_or(RuntimeError::Invariant( + "ordinary shape index is out of bounds", + ))?; + if data.slots.get(index).is_none() { + return Err(RuntimeError::Invariant( + "ordinary shape has no parallel slot", + )); + } + Ok(Some(OwnSlot { + index, + flags: entry.flags, + })) +} + +impl Runtime { + /// None means that another semantic owner must handle the write. This + /// method never consumes the value or invokes a callback before declining. + pub(super) fn try_set_ordinary_own_data( + &self, + object: &ObjectRef, + key: &PropertyKey, + value: &Value, + ) -> Result, RuntimeError> { + self.validate_object_and_key(object, key)?; + self.validate_value_domain(value, "property value")?; + let mut state = self.0.state.borrow_mut(); + let id = object.object_id(); + if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + return Ok(None); + } + let Some(slot) = locate(&state, id, key.atom())? else { + return Ok(None); + }; + if !matches!( + state.heap.object(id)?.slots[slot.index], + PropertySlot::Data(_) + ) { + return Ok(None); + } + if !slot.flags.writable { + return Ok(Some(false)); + } + // Conversion only duplicates primitive storage/identities. The input + // remains rooted by the caller throughout retain, publication and drain. + let replacement = PropertySlot::Data(self.raw_property_value(value)?); + let atoms = state.retain_slot_atoms(std::slice::from_ref(&replacement))?; + match state.heap.replace_object_slot(id, slot.index, replacement) { + Ok(cleanup) => state.apply_cleanup(cleanup)?, + Err(error) => { + state.release_atoms(atoms)?; + return Err(error.into()); + } + } + Ok(Some(true)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::object::{DescriptorField, OrdinaryPropertyDescriptor}; + + #[test] + fn ordinary_property_replacement_preserves_roots_and_rejects_foreign_values() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let object = runtime.new_object(None).unwrap(); + let old = runtime.new_object(None).unwrap(); + let key = runtime.intern_property_key("x").unwrap(); + runtime + .define_own_property( + &object, + &key, + &OrdinaryPropertyDescriptor { + value: DescriptorField::Present(Value::Object(old.clone())), + writable: DescriptorField::Present(true), + configurable: DescriptorField::Present(true), + ..OrdinaryPropertyDescriptor::new() + }, + ) + .unwrap(); + assert_eq!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(old.object_id()), + Ok(2) + ); + assert!(context.set_property(&object, &key, Value::Int(42)).unwrap()); + assert_eq!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(old.object_id()), + Ok(1) + ); + let foreign = Runtime::new().new_object(None).unwrap(); + assert!( + context + .set_property(&object, &key, Value::Object(foreign)) + .is_err() + ); + assert_eq!(context.get_property(&object, &key).unwrap(), Value::Int(42)); + } + + #[test] + fn ordinary_property_own_write_stops_before_revoked_proxy_prototype() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert_eq!( + context + .eval( + r#" + var rev = Proxy.revocable({}, {}); + var obj = Object.create(rev.proxy); + Object.defineProperty(obj, 'x', {value: 1, writable: true}); + rev.revoke(); + obj.x = 42; + obj.x; + "# + ) + .unwrap(), + Value::Int(42) + ); + } +} From 5ad6436e258738883a6516523ea0bd110bf8ac86 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 14:16:45 +0800 Subject: [PATCH 03/14] refactor(object): unify ordinary Set and traverse prototypes on demand --- src/engine/heap/runtime/tests.rs | 4 +- src/engine/object/internal_methods.rs | 188 +++---------------- src/engine/object/mod.rs | 2 + src/engine/object/operations.rs | 1 + src/engine/object/ordinary.rs | 251 ++++++++++++++++++++++++++ src/engine/object/ordinary_storage.rs | 107 +++++++---- src/engine/object/properties.rs | 141 +-------------- 7 files changed, 356 insertions(+), 338 deletions(-) create mode 100644 src/engine/object/ordinary.rs diff --git a/src/engine/heap/runtime/tests.rs b/src/engine/heap/runtime/tests.rs index ff000086..9bc5504a 100644 --- a/src/engine/heap/runtime/tests.rs +++ b/src/engine/heap/runtime/tests.rs @@ -492,7 +492,7 @@ fn set_property( ) -> Result { match runtime.prepare_set_property(object, key, value)? { PropertySetAction::Complete => Ok(true), - PropertySetAction::Rejected(_) => Ok(false), + PropertySetAction::Rejected(_) | PropertySetAction::RejectedProxyTrap => Ok(false), PropertySetAction::Throw(_) => Err(RuntimeError::Invariant( "context-free property test produced a JavaScript throw", )), @@ -511,7 +511,7 @@ fn set_property_with_receiver( ) -> Result { match runtime.prepare_set_property_with_receiver(object, key, value, receiver)? { PropertySetAction::Complete => Ok(true), - PropertySetAction::Rejected(_) => Ok(false), + PropertySetAction::Rejected(_) | PropertySetAction::RejectedProxyTrap => Ok(false), PropertySetAction::Throw(_) => Err(RuntimeError::Invariant( "context-free property test produced a JavaScript throw", )), diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index d48faaef..60dbe03b 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -15,7 +15,7 @@ use crate::engine::atom::{Atom, PropertyKeyKind}; use crate::engine::builtins::CanonicalNumericIndex; use crate::engine::heap::{ContextId, ObjectPayload, ProxyData}; use crate::engine::object::operations::{ - ArrayOwnKey, InternalDefineResult, InternalSetResult, PropertyDefineOutcome, PropertySetAction, + InternalDefineResult, InternalSetResult, PropertyDefineOutcome, PropertySetAction, PropertySetRejection, complete_to_validation_record, descriptor_to_validation_record, validation_record_to_complete, }; @@ -1001,29 +1001,6 @@ impl Runtime { Ok(Completion::Return(result)) } - fn ordinary_set_fast_path_available( - &self, - object: &ObjectRef, - receiver: &Value, - ) -> Result { - if let Value::Object(receiver) = receiver - && (self.proxy_snapshot_if_any(receiver)?.is_some() - || self.typed_array_is_object(receiver)?) - { - return Ok(false); - } - let mut current = Some(object.clone()); - while let Some(object) = current { - if self.proxy_snapshot_if_any(&object)?.is_some() - || self.typed_array_is_object(&object)? - { - return Ok(false); - } - current = self.get_prototype_of(&object)?; - } - Ok(true) - } - pub(crate) fn internal_set( &self, realm: ContextId, @@ -1032,15 +1009,10 @@ impl Runtime { value: Value, receiver: Value, ) -> Result, RuntimeError> { - if matches!(&receiver, Value::Object(target) if target == object) - && let Some(accepted) = self.try_set_ordinary_own_data(object, key, &value)? - { - return Ok(NativeConversion::Value(if accepted { - InternalSetResult::Accepted - } else { - InternalSetResult::Rejected(PropertySetRejection::ReadOnly) - })); - } + let _operation = self.operation(); + self.validate_object_and_key(object, key)?; + self.validate_value_domain(&value, "property value")?; + self.validate_value_domain(&receiver, "property receiver")?; if self.proxy_snapshot_if_any(object)?.is_some() { return self.proxy_set(realm, object, key, value, receiver); } @@ -1089,138 +1061,26 @@ impl Runtime { } } } - if self.ordinary_set_fast_path_available(object, &receiver)? { - return match self.prepare_set_property_with_receiver_in_realm( - Some(realm), - object, - key, - value, - receiver, - )? { - PropertySetAction::Complete => { - Ok(NativeConversion::Value(InternalSetResult::Accepted)) - } - PropertySetAction::Throw(value) => Ok(NativeConversion::Throw(value)), - PropertySetAction::Rejected(rejection) => Ok(NativeConversion::Value( - InternalSetResult::Rejected(rejection), - )), - PropertySetAction::Call { - setter, - receiver, - argument, - } => match self.call_internal(realm, &setter, receiver, &[argument])? { - Completion::Return(_) => { - Ok(NativeConversion::Value(InternalSetResult::Accepted)) - } - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), - }, - }; - } - - let own = match self.internal_get_own_property(realm, object, key)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), - }; - let own = if let Some(own) = own { - own - } else { - let prototype = match self.internal_get_prototype_of(realm, object)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), - }; - if let Some(prototype) = prototype { - return self.internal_set(realm, &prototype, key, value, receiver); - } - CompleteOrdinaryPropertyDescriptor::Data { - value: Value::Undefined, - writable: true, - enumerable: true, - configurable: true, - } - }; - - match own { - CompleteOrdinaryPropertyDescriptor::Data { - writable: false, .. - } => Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::ReadOnly, - ))), - CompleteOrdinaryPropertyDescriptor::Data { .. } => { - let Value::Object(receiver) = receiver else { - return Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::NotObject, - ))); - }; - let existing = match self.internal_get_own_property(realm, &receiver, key)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), - }; - let descriptor = match existing { - Some(CompleteOrdinaryPropertyDescriptor::Accessor { set: None, .. }) => { - return Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::NoSetter, - ))); - } - Some(CompleteOrdinaryPropertyDescriptor::Accessor { set: Some(_), .. }) => { - return Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::ReadOnly, - ))); - } - Some(CompleteOrdinaryPropertyDescriptor::Data { - writable: false, .. - }) => { - return Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::ReadOnly, - ))); - } - Some(CompleteOrdinaryPropertyDescriptor::Data { .. }) => { - OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), - ..OrdinaryPropertyDescriptor::new() - } - } - None => OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), - writable: DescriptorField::Present(true), - enumerable: DescriptorField::Present(true), - configurable: DescriptorField::Present(true), - ..OrdinaryPropertyDescriptor::new() - }, - }; - match self.internal_define_own_property(realm, &receiver, key, &descriptor)? { - NativeConversion::Value(InternalDefineResult::Defined) => { - Ok(NativeConversion::Value(InternalSetResult::Accepted)) - } - NativeConversion::Value(InternalDefineResult::RejectedProxyTrap) => Ok( - NativeConversion::Value(InternalSetResult::RejectedProxyTrap), - ), - NativeConversion::Value(InternalDefineResult::RejectedOrdinary(object)) => { - let rejection = if !self.has_own_property(&object, key)? - && !self.is_extensible(&object)? - { - PropertySetRejection::NotExtensible - } else if matches!(self.array_own_key(&object, key)?, ArrayOwnKey::Index(_)) - && !self.array_length_state(&object)?.1 - { - PropertySetRejection::ArrayLengthReadOnly - } else { - PropertySetRejection::ReadOnly - }; - Ok(NativeConversion::Value(InternalSetResult::Rejected( - rejection, - ))) - } - NativeConversion::Throw(value) => Ok(NativeConversion::Throw(value)), - } - } - CompleteOrdinaryPropertyDescriptor::Accessor { set: None, .. } => { - Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::NoSetter, - ))) + match self.prepare_set_property_with_receiver_in_realm( + Some(realm), + object, + key, + value, + receiver, + )? { + PropertySetAction::Complete => Ok(NativeConversion::Value(InternalSetResult::Accepted)), + PropertySetAction::RejectedProxyTrap => Ok(NativeConversion::Value( + InternalSetResult::RejectedProxyTrap, + )), + PropertySetAction::Throw(value) => Ok(NativeConversion::Throw(value)), + PropertySetAction::Rejected(reason) => { + Ok(NativeConversion::Value(InternalSetResult::Rejected(reason))) } - CompleteOrdinaryPropertyDescriptor::Accessor { - set: Some(setter), .. - } => match self.call_internal(realm, &setter, receiver, &[value])? { + PropertySetAction::Call { + setter, + receiver, + argument, + } => match self.call_internal(realm, &setter, receiver, &[argument])? { Completion::Return(_) => Ok(NativeConversion::Value(InternalSetResult::Accepted)), Completion::Throw(value) => Ok(NativeConversion::Throw(value)), }, diff --git a/src/engine/object/mod.rs b/src/engine/object/mod.rs index 61847e5a..576b79b2 100644 --- a/src/engine/object/mod.rs +++ b/src/engine/object/mod.rs @@ -750,3 +750,5 @@ pub(crate) mod storage; pub(crate) mod operations; mod ordinary_storage; + +mod ordinary; diff --git a/src/engine/object/operations.rs b/src/engine/object/operations.rs index 9e0cb6fc..e9b58bec 100644 --- a/src/engine/object/operations.rs +++ b/src/engine/object/operations.rs @@ -46,6 +46,7 @@ pub(crate) enum PropertyGetAction { } pub(crate) enum PropertySetAction { + RejectedProxyTrap, Complete, Rejected(PropertySetRejection), Throw(Value), diff --git a/src/engine/object/ordinary.rs b/src/engine/object/ordinary.rs new file mode 100644 index 00000000..4505ef57 --- /dev/null +++ b/src/engine/object/ordinary.rs @@ -0,0 +1,251 @@ +//! Ordinary Set semantics. Storage probes finish before observable calls; +//! exceptional receivers retain the internal-method dispatch contract. +use crate::engine::api::runtime::Runtime; +use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::heap::ContextId; +use crate::engine::object::operations::{ + ArrayOwnKey, InternalDefineResult, InternalSetResult, PropertyDefineOutcome, PropertySetAction, + PropertySetRejection, +}; +use crate::engine::object::ordinary_storage::SetProbe; +use crate::engine::object::{ + CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, + PropertyKey, +}; +use crate::engine::value::Value; +use crate::engine::value::conversion::NativeConversion; + +impl Runtime { + #[cfg(test)] + pub(crate) fn prepare_set_property( + &self, + object: &ObjectRef, + key: &PropertyKey, + value: Value, + ) -> Result { + let _operation = self.operation(); + self.prepare_set_property_with_receiver_in_realm( + None, + object, + key, + value, + Value::Object(object.clone()), + ) + } + + #[cfg(test)] + pub(crate) fn prepare_set_property_with_receiver( + &self, + object: &ObjectRef, + key: &PropertyKey, + value: Value, + receiver: Value, + ) -> Result { + self.prepare_set_property_with_receiver_in_realm(None, object, key, value, receiver) + } + + pub(crate) fn prepare_set_property_with_receiver_in_realm( + &self, + realm: Option, + object: &ObjectRef, + key: &PropertyKey, + value: Value, + receiver: Value, + ) -> Result { + let _operation = self.operation(); + self.validate_object_and_key(object, key)?; + self.validate_value_domain(&value, "property value")?; + self.validate_value_domain(&receiver, "property receiver")?; + let mut cursor = Some(object.clone()); + let mut first = true; + while let Some(current) = cursor { + let same_receiver = matches!(&receiver, Value::Object(target) if target == ¤t); + match self.ordinary_set_probe(¤t, key, &value, same_receiver)? { + SetProbe::Stored(accepted) => { + return Ok(if accepted { + PropertySetAction::Complete + } else { + PropertySetAction::Rejected(PropertySetRejection::ReadOnly) + }); + } + SetProbe::Writable => break, + SetProbe::Setter(set) => { + return Ok(match set { + Some(setter) => PropertySetAction::Call { + setter, + receiver, + argument: value, + }, + None => PropertySetAction::Rejected(PropertySetRejection::NoSetter), + }); + } + SetProbe::Missing(prototype) => { + cursor = prototype; + first = false; + continue; + } + SetProbe::Special => { + // Only an encountered exotic prototype delegates. The initial + // object's special Set has already been selected by internal_set. + if !first + && (self.is_proxy_object(¤t)? + || self.typed_array_is_object(¤t)? + || self.is_module_namespace_object(¤t)?) + { + let realm = + realm.ok_or(RuntimeError::Invariant("exotic Set requires a realm"))?; + return Ok(set_completion( + self.internal_set(realm, ¤t, key, value, receiver)?, + )); + } + } + } + if let Some(property) = self.get_own_property(¤t, key)? { + match property { + CompleteOrdinaryPropertyDescriptor::Data { writable, .. } => { + if same_receiver + && self.array_own_key(¤t, key)? == ArrayOwnKey::Length + { + return self.prepare_set_array_length(realm, ¤t, key, value); + } + if !writable { + return Ok(PropertySetAction::Rejected(PropertySetRejection::ReadOnly)); + } + break; + } + CompleteOrdinaryPropertyDescriptor::Accessor { set, .. } => { + return Ok(match set { + Some(setter) => PropertySetAction::Call { + setter, + receiver, + argument: value, + }, + None => PropertySetAction::Rejected(PropertySetRejection::NoSetter), + }); + } + } + } + cursor = self.get_prototype_of(¤t)?; + first = false; + } + let Value::Object(receiver) = receiver else { + return Ok(PropertySetAction::Rejected(PropertySetRejection::NotObject)); + }; + // A receiver is a distinct lookup. Its accessor is rejected, never + // called, even when the inherited target descriptor was writable. + match self.ordinary_set_probe(&receiver, key, &value, true)? { + SetProbe::Stored(accepted) => { + return Ok(if accepted { + PropertySetAction::Complete + } else { + PropertySetAction::Rejected(PropertySetRejection::ReadOnly) + }); + } + SetProbe::Setter(set) => { + return Ok(PropertySetAction::Rejected(if set.is_some() { + PropertySetRejection::ReadOnly + } else { + PropertySetRejection::NoSetter + })); + } + SetProbe::Missing(_) => { + return self.define_set_receiver(realm, &receiver, key, value, false); + } + SetProbe::Special => {} + SetProbe::Writable => unreachable!("receiver probe commits a writable data slot"), + } + let existing = match realm { + Some(realm) => match self.internal_get_own_property(realm, &receiver, key)? { + NativeConversion::Value(existing) => existing, + NativeConversion::Throw(value) => return Ok(PropertySetAction::Throw(value)), + }, + None => self.get_own_property(&receiver, key)?, + }; + match existing { + Some(CompleteOrdinaryPropertyDescriptor::Data { + writable: false, .. + }) => Ok(PropertySetAction::Rejected(PropertySetRejection::ReadOnly)), + Some(CompleteOrdinaryPropertyDescriptor::Accessor { set, .. }) => { + Ok(PropertySetAction::Rejected(if set.is_some() { + PropertySetRejection::ReadOnly + } else { + PropertySetRejection::NoSetter + })) + } + Some(CompleteOrdinaryPropertyDescriptor::Data { .. }) => { + if self.set_arguments_index_value(&receiver, key, &value)? { + return Ok(PropertySetAction::Complete); + } + self.define_set_receiver(realm, &receiver, key, value, true) + } + None => self.define_set_receiver(realm, &receiver, key, value, false), + } + } + + fn define_set_receiver( + &self, + realm: Option, + receiver: &ObjectRef, + key: &PropertyKey, + value: Value, + existing: bool, + ) -> Result { + let descriptor = if existing { + OrdinaryPropertyDescriptor { + value: DescriptorField::Present(value), + ..OrdinaryPropertyDescriptor::new() + } + } else { + OrdinaryPropertyDescriptor { + value: DescriptorField::Present(value), + writable: DescriptorField::Present(true), + enumerable: DescriptorField::Present(true), + configurable: DescriptorField::Present(true), + ..OrdinaryPropertyDescriptor::new() + } + }; + let defined = match realm { + Some(realm) => { + match self.internal_define_own_property(realm, receiver, key, &descriptor)? { + NativeConversion::Value(InternalDefineResult::Defined) => true, + NativeConversion::Value(InternalDefineResult::RejectedProxyTrap) => { + return Ok(PropertySetAction::RejectedProxyTrap); + } + NativeConversion::Value(InternalDefineResult::RejectedOrdinary(_)) => false, + NativeConversion::Throw(value) => return Ok(PropertySetAction::Throw(value)), + } + } + None => match self.define_own_property_in_realm(None, receiver, key, &descriptor)? { + PropertyDefineOutcome::Defined(defined) => defined, + PropertyDefineOutcome::Throw(value) => return Ok(PropertySetAction::Throw(value)), + }, + }; + if defined { + return Ok(PropertySetAction::Complete); + } + Ok(PropertySetAction::Rejected( + if !self.has_own_property(receiver, key)? && !self.is_extensible(receiver)? { + PropertySetRejection::NotExtensible + } else if matches!(self.array_own_key(receiver, key)?, ArrayOwnKey::Index(_)) + && !self.array_length_state(receiver)?.1 + { + PropertySetRejection::ArrayLengthReadOnly + } else { + PropertySetRejection::ReadOnly + }, + )) + } +} + +fn set_completion(result: NativeConversion) -> PropertySetAction { + match result { + NativeConversion::Throw(value) => PropertySetAction::Throw(value), + NativeConversion::Value(InternalSetResult::Accepted) => PropertySetAction::Complete, + NativeConversion::Value(InternalSetResult::Rejected(reason)) => { + PropertySetAction::Rejected(reason) + } + NativeConversion::Value(InternalSetResult::RejectedProxyTrap) => { + PropertySetAction::RejectedProxyTrap + } + } +} diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index 5458f6f9..faf488c5 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -40,46 +40,89 @@ fn locate( })) } +pub(super) enum SetProbe { + Stored(bool), + Writable, + Setter(Option), + Missing(Option), + Special, +} + impl Runtime { - /// None means that another semantic owner must handle the write. This - /// method never consumes the value or invokes a callback before declining. - pub(super) fn try_set_ordinary_own_data( + /// The target stays rooted until the selected setter/prototype has been + /// promoted. No callback, mutation or cleanup occurs between snapshot and + /// promotion. Data writes locate and commit in a single mutable borrow. + pub(super) fn ordinary_set_probe( &self, object: &ObjectRef, key: &PropertyKey, value: &Value, - ) -> Result, RuntimeError> { - self.validate_object_and_key(object, key)?; - self.validate_value_domain(value, "property value")?; - let mut state = self.0.state.borrow_mut(); - let id = object.object_id(); - if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { - return Ok(None); - } - let Some(slot) = locate(&state, id, key.atom())? else { - return Ok(None); - }; - if !matches!( - state.heap.object(id)?.slots[slot.index], - PropertySlot::Data(_) - ) { - return Ok(None); - } - if !slot.flags.writable { - return Ok(Some(false)); + receiver_is_target: bool, + ) -> Result { + enum Selected { + Setter(Option), + Missing(Option), } - // Conversion only duplicates primitive storage/identities. The input - // remains rooted by the caller throughout retain, publication and drain. - let replacement = PropertySlot::Data(self.raw_property_value(value)?); - let atoms = state.retain_slot_atoms(std::slice::from_ref(&replacement))?; - match state.heap.replace_object_slot(id, slot.index, replacement) { - Ok(cleanup) => state.apply_cleanup(cleanup)?, - Err(error) => { - state.release_atoms(atoms)?; - return Err(error.into()); + let selected = { + let mut state = self.0.state.borrow_mut(); + let id = object.object_id(); + if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + return Ok(SetProbe::Special); } + match locate(&state, id, key.atom())? { + None => { + let data = state.heap.object(id)?; + Selected::Missing(state.heap.shape(data.shape)?.prototype()) + } + Some(slot) => match &state.heap.object(id)?.slots[slot.index] { + PropertySlot::Data(_) => { + if !slot.flags.writable { + return Ok(SetProbe::Stored(false)); + } + if !receiver_is_target { + return Ok(SetProbe::Writable); + } + let replacement = PropertySlot::Data(self.raw_property_value(value)?); + replace_data(&mut state, id, slot, replacement)?; + return Ok(SetProbe::Stored(true)); + } + PropertySlot::Accessor { set, .. } => Selected::Setter(*set), + PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) => { + return Ok(SetProbe::Special); + } + }, + } + }; + Ok(match selected { + Selected::Setter(set) => SetProbe::Setter( + set.map(|id| { + ObjectRef::from_borrowed_handle(self.clone(), id) + .map(crate::engine::object::CallableRef::from_validated_object) + }) + .transpose()?, + ), + Selected::Missing(prototype) => SetProbe::Missing( + prototype + .map(|id| ObjectRef::from_borrowed_handle(self.clone(), id)) + .transpose()?, + ), + }) + } +} + +fn replace_data( + state: &mut RuntimeState, + id: ObjectId, + slot: OwnSlot, + replacement: PropertySlot, +) -> Result<(), RuntimeError> { + let atoms = state.retain_slot_atoms(std::slice::from_ref(&replacement))?; + match state.heap.replace_object_slot(id, slot.index, replacement) { + Ok(cleanup) => state.apply_cleanup(cleanup), + Err(error) => { + state.release_atoms(atoms)?; + Err(error.into()) } - Ok(Some(true)) } } diff --git a/src/engine/object/properties.rs b/src/engine/object/properties.rs index ff8966f7..75bf9dfa 100644 --- a/src/engine/object/properties.rs +++ b/src/engine/object/properties.rs @@ -497,149 +497,10 @@ impl Runtime { Ok(None) } - #[cfg(test)] - pub(crate) fn prepare_set_property( - &self, - object: &ObjectRef, - key: &PropertyKey, - value: Value, - ) -> Result { - let _operation = self.operation(); - self.prepare_set_property_with_receiver_in_realm( - None, - object, - key, - value, - Value::Object(object.clone()), - ) - } - - #[cfg(test)] - pub(crate) fn prepare_set_property_with_receiver( - &self, - object: &ObjectRef, - key: &PropertyKey, - value: Value, - receiver: Value, - ) -> Result { - self.prepare_set_property_with_receiver_in_realm(None, object, key, value, receiver) - } - - pub(crate) fn prepare_set_property_with_receiver_in_realm( - &self, - realm: Option, - object: &ObjectRef, - key: &PropertyKey, - value: Value, - receiver: Value, - ) -> Result { - let _operation = self.operation(); - self.validate_object_and_key(object, key)?; - self.validate_value_domain(&value, "property value")?; - self.validate_value_domain(&receiver, "property receiver")?; - let mut cursor = Some(object.clone()); - let mut inherited_allows_write = true; - let mut direct_array_length = false; - while let Some(current) = cursor { - if let Some(property) = self.get_own_property(¤t, key)? { - match property { - CompleteOrdinaryPropertyDescriptor::Data { writable, .. } => { - direct_array_length = matches!(&receiver, Value::Object(receiver) - if receiver == ¤t - && self.array_own_key(¤t, key)? == ArrayOwnKey::Length); - inherited_allows_write = writable; - break; - } - CompleteOrdinaryPropertyDescriptor::Accessor { set: None, .. } => { - return Ok(PropertySetAction::Rejected(PropertySetRejection::NoSetter)); - } - CompleteOrdinaryPropertyDescriptor::Accessor { - set: Some(setter), .. - } => { - return Ok(PropertySetAction::Call { - setter, - receiver, - argument: value, - }); - } - } - } - cursor = self.get_prototype_of(¤t)?; - } - if direct_array_length { - let Value::Object(receiver) = receiver else { - return Err(RuntimeError::Invariant( - "direct Array length write lost its object receiver", - )); - }; - return self.prepare_set_array_length(realm, &receiver, key, value); - } - if !inherited_allows_write { - return Ok(PropertySetAction::Rejected(PropertySetRejection::ReadOnly)); - } - - let Value::Object(receiver) = receiver else { - return Ok(PropertySetAction::Rejected(PropertySetRejection::NotObject)); - }; - let descriptor = match self.get_own_property(&receiver, key)? { - Some(CompleteOrdinaryPropertyDescriptor::Data { - writable: false, .. - }) => { - return Ok(PropertySetAction::Rejected(PropertySetRejection::ReadOnly)); - } - Some(CompleteOrdinaryPropertyDescriptor::Accessor { set: None, .. }) => { - return Ok(PropertySetAction::Rejected(PropertySetRejection::NoSetter)); - } - Some(CompleteOrdinaryPropertyDescriptor::Accessor { set: Some(_), .. }) => { - return Ok(PropertySetAction::Rejected(PropertySetRejection::ReadOnly)); - } - Some(CompleteOrdinaryPropertyDescriptor::Data { .. }) => { - if self.set_arguments_index_value(&receiver, key, &value)? { - return Ok(PropertySetAction::Complete); - } - OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), - ..OrdinaryPropertyDescriptor::new() - } - } - None => { - if !self.is_extensible(&receiver)? { - return Ok(PropertySetAction::Rejected( - PropertySetRejection::NotExtensible, - )); - } - OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), - writable: DescriptorField::Present(true), - enumerable: DescriptorField::Present(true), - configurable: DescriptorField::Present(true), - ..OrdinaryPropertyDescriptor::new() - } - } - }; - Ok( - match self.define_own_property_in_realm(realm, &receiver, key, &descriptor)? { - PropertyDefineOutcome::Defined(true) => PropertySetAction::Complete, - PropertyDefineOutcome::Defined(false) => { - let rejection = - if matches!(self.array_own_key(&receiver, key)?, ArrayOwnKey::Index(_)) - && !self.array_length_state(&receiver)?.1 - { - PropertySetRejection::ArrayLengthReadOnly - } else { - PropertySetRejection::ReadOnly - }; - PropertySetAction::Rejected(rejection) - } - PropertyDefineOutcome::Throw(value) => PropertySetAction::Throw(value), - }, - ) - } - /// Array `length` assignment has the same conversion and deletion kernel /// as DefineOwnProperty, but Set must reject every write once length is /// read-only, including a SameValue write that DefineOwnProperty accepts. - fn prepare_set_array_length( + pub(super) fn prepare_set_array_length( &self, realm: Option, object: &ObjectRef, From 68ce00f04b2f858528143eeb259548d043baefb4 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 14:22:50 +0800 Subject: [PATCH 04/14] perf(object): share ordinary reads and metadata lookup --- src/engine/object/internal_methods.rs | 68 ++++++-------- src/engine/object/mod.rs | 3 + src/engine/object/ordinary.rs | 48 +++++++++- src/engine/object/ordinary_storage.rs | 97 +++++++++++++++++++ src/engine/object/ordinary_tests.rs | 130 ++++++++++++++++++++++++++ src/engine/object/properties.rs | 21 +++++ 6 files changed, 327 insertions(+), 40 deletions(-) create mode 100644 src/engine/object/ordinary_tests.rs diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 60dbe03b..55532f34 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -556,6 +556,15 @@ impl Runtime { object: &ObjectRef, key: &PropertyKey, ) -> Result, RuntimeError> { + if let Some(own) = self.ordinary_property_flags(object, key)? { + match own { + None => return Ok(NativeConversion::Value(false)), + Some(own) if !own.needs_materialization => { + return Ok(NativeConversion::Value(own.flags.enumerable)); + } + Some(_) => {} + } + } Ok(match self.internal_get_own_property(realm, object, key)? { NativeConversion::Value(Some(descriptor)) => { NativeConversion::Value(descriptor.enumerable()) @@ -838,45 +847,14 @@ impl Runtime { key: &PropertyKey, receiver: Value, ) -> Result { - if self.proxy_snapshot_if_any(object)?.is_some() { - return self.proxy_get(realm, object, key, receiver); - } - if self.typed_array_is_object(object)? - && let Some(numeric) = self.typed_array_canonical_numeric_index(key)? - { - let value = match numeric { - CanonicalNumericIndex::Valid(index) => self - .typed_array_read_index(object, index)? - .unwrap_or(Value::Undefined), - CanonicalNumericIndex::Invalid => Value::Undefined, - }; - return Ok(Completion::Return(value)); - } - let own = match self.internal_get_own_property(realm, object, key)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), - }; - if let Some(own) = own { - return match own { - CompleteOrdinaryPropertyDescriptor::Data { value, .. } => { - Ok(Completion::Return(value)) - } - CompleteOrdinaryPropertyDescriptor::Accessor { get: None, .. } => { - Ok(Completion::Return(Value::Undefined)) + Ok( + match self.internal_get_or_missing(realm, object, key, receiver)? { + NativeConversion::Value(value) => { + Completion::Return(value.unwrap_or(Value::Undefined)) } - CompleteOrdinaryPropertyDescriptor::Accessor { - get: Some(getter), .. - } => self.call_internal(realm, &getter, receiver, &[]), - }; - } - let prototype = match self.internal_get_prototype_of(realm, object)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), - }; - let Some(prototype) = prototype else { - return Ok(Completion::Return(Value::Undefined)); - }; - self.internal_get(realm, &prototype, key, receiver) + NativeConversion::Throw(value) => Completion::Throw(value), + }, + ) } /// Completion-aware property read which preserves QuickJS's internal @@ -894,9 +872,21 @@ impl Runtime { object: &ObjectRef, key: &PropertyKey, receiver: Value, + ) -> Result>, RuntimeError> { + self.validate_object_and_key(object, key)?; + self.validate_value_domain(&receiver, "property receiver")?; + self.get_ordinary_chain(realm, object, key, receiver) + } + + pub(super) fn get_special_or_missing( + &self, + realm: ContextId, + object: &ObjectRef, + key: &PropertyKey, + receiver: Value, ) -> Result>, RuntimeError> { if self.proxy_snapshot_if_any(object)?.is_some() { - return Ok(match self.internal_get(realm, object, key, receiver)? { + return Ok(match self.proxy_get(realm, object, key, receiver)? { Completion::Return(value) => NativeConversion::Value(Some(value)), Completion::Throw(value) => NativeConversion::Throw(value), }); diff --git a/src/engine/object/mod.rs b/src/engine/object/mod.rs index 576b79b2..7af14279 100644 --- a/src/engine/object/mod.rs +++ b/src/engine/object/mod.rs @@ -752,3 +752,6 @@ pub(crate) mod operations; mod ordinary_storage; mod ordinary; + +#[cfg(test)] +mod ordinary_tests; diff --git a/src/engine/object/ordinary.rs b/src/engine/object/ordinary.rs index 4505ef57..511e720e 100644 --- a/src/engine/object/ordinary.rs +++ b/src/engine/object/ordinary.rs @@ -56,6 +56,12 @@ impl Runtime { self.validate_object_and_key(object, key)?; self.validate_value_domain(&value, "property value")?; self.validate_value_domain(&receiver, "property receiver")?; + if realm.is_none() + && (self.is_proxy_object(object)? + || matches!(&receiver, Value::Object(object) if self.is_proxy_object(object)?)) + { + return Err(RuntimeError::Invariant("exotic Set requires a realm")); + } let mut cursor = Some(object.clone()); let mut first = true; while let Some(current) = cursor { @@ -204,6 +210,7 @@ impl Runtime { ..OrdinaryPropertyDescriptor::new() } }; + let mut rejected_object = None; let defined = match realm { Some(realm) => { match self.internal_define_own_property(realm, receiver, key, &descriptor)? { @@ -211,7 +218,10 @@ impl Runtime { NativeConversion::Value(InternalDefineResult::RejectedProxyTrap) => { return Ok(PropertySetAction::RejectedProxyTrap); } - NativeConversion::Value(InternalDefineResult::RejectedOrdinary(_)) => false, + NativeConversion::Value(InternalDefineResult::RejectedOrdinary(object)) => { + rejected_object = Some(object); + false + } NativeConversion::Throw(value) => return Ok(PropertySetAction::Throw(value)), } } @@ -223,6 +233,7 @@ impl Runtime { if defined { return Ok(PropertySetAction::Complete); } + let receiver = rejected_object.as_ref().unwrap_or(receiver); Ok(PropertySetAction::Rejected( if !self.has_own_property(receiver, key)? && !self.is_extensible(receiver)? { PropertySetRejection::NotExtensible @@ -249,3 +260,38 @@ fn set_completion(result: NativeConversion) -> PropertySetAct } } } + +impl Runtime { + /// Ordinary nodes are iterative. A special node delegates exactly once and + /// preserves the distinction between missing and an observed undefined. + pub(super) fn get_ordinary_chain( + &self, + realm: ContextId, + object: &ObjectRef, + key: &PropertyKey, + receiver: Value, + ) -> Result>, RuntimeError> { + use crate::engine::object::ordinary_storage::ReadProbe; + use crate::engine::vm::Completion; + let mut current = object.clone(); + loop { + match self.ordinary_read_probe(¤t, key)? { + ReadProbe::Value(value) => return Ok(NativeConversion::Value(Some(value))), + ReadProbe::Getter(None) => { + return Ok(NativeConversion::Value(Some(Value::Undefined))); + } + ReadProbe::Getter(Some(getter)) => { + return Ok(match self.call_internal(realm, &getter, receiver, &[])? { + Completion::Return(value) => NativeConversion::Value(Some(value)), + Completion::Throw(value) => NativeConversion::Throw(value), + }); + } + ReadProbe::Missing(Some(prototype)) => current = prototype, + ReadProbe::Missing(None) => return Ok(NativeConversion::Value(None)), + ReadProbe::Special => { + return self.get_special_or_missing(realm, ¤t, key, receiver); + } + } + } + } +} diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index faf488c5..3065ce7e 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -126,6 +126,103 @@ fn replace_data( } } +pub(super) enum ReadProbe { + Value(Value), + Getter(Option), + Missing(Option), + Special, +} + +pub(super) struct OwnFlags { + pub(super) flags: PropertyFlags, + pub(super) needs_materialization: bool, +} + +impl Runtime { + pub(super) fn ordinary_property_flags( + &self, + object: &ObjectRef, + key: &PropertyKey, + ) -> Result>, RuntimeError> { + self.validate_object_and_key(object, key)?; + let state = self.0.state.borrow(); + let id = object.object_id(); + if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + return Ok(None); + } + Ok(Some(locate(&state, id, key.atom())?.map(|slot| OwnFlags { + flags: slot.flags, + needs_materialization: matches!( + state.heap.object(id).expect("located live object").slots[slot.index], + PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) + ), + }))) + } + + pub(super) fn ordinary_property_snapshot( + &self, + object: &ObjectRef, + key: &PropertyKey, + ) -> Result>, RuntimeError> + { + use crate::engine::object::operations::PropertySnapshot; + let state = self.0.state.borrow(); + let id = object.object_id(); + if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + return Ok(None); + } + let Some(slot) = locate(&state, id, key.atom())? else { + return Ok(Some(None)); + }; + let flags = slot.flags; + Ok(Some(Some( + match &state.heap.object(id)?.slots[slot.index] { + PropertySlot::Data(value) => PropertySnapshot::Data { + value: value.clone(), + flags, + }, + PropertySlot::Accessor { get, set } => PropertySnapshot::Accessor { + get: *get, + set: *set, + flags, + }, + PropertySlot::VarRef(var_ref) => PropertySnapshot::VarRef { + var_ref: *var_ref, + flags, + }, + PropertySlot::AutoInit(_) => PropertySnapshot::AutoInit, + }, + ))) + } + + pub(super) fn ordinary_read_probe( + &self, + object: &ObjectRef, + key: &PropertyKey, + ) -> Result { + use crate::engine::object::operations::PropertySnapshot; + let Some(snapshot) = self.ordinary_property_snapshot(object, key)? else { + return Ok(ReadProbe::Special); + }; + Ok(match snapshot { + Some(PropertySnapshot::Data { value, .. }) => { + ReadProbe::Value(self.root_raw_value(&value)?) + } + Some(PropertySnapshot::Accessor { get, .. }) => ReadProbe::Getter( + get.map(|id| { + ObjectRef::from_borrowed_handle(self.clone(), id) + .map(crate::engine::object::CallableRef::from_validated_object) + }) + .transpose()?, + ), + Some(PropertySnapshot::AutoInit | PropertySnapshot::VarRef { .. }) => { + ReadProbe::Special + } + None => ReadProbe::Missing(self.get_prototype_of(object)?), + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/engine/object/ordinary_tests.rs b/src/engine/object/ordinary_tests.rs new file mode 100644 index 00000000..ba36cc28 --- /dev/null +++ b/src/engine/object/ordinary_tests.rs @@ -0,0 +1,130 @@ +//! Observable regression coverage for the shared ordinary property kernel. +use crate::engine::api::runtime::Runtime; +use crate::engine::value::Value; + +fn check(source: &str) { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert_eq!(context.eval(source).unwrap(), Value::Bool(true)); +} + +#[test] +fn ordinary_property_receiver_proxy_preserves_traps_and_rejection_object() { + check( + r#" + var log=[]; var target={x:0}; + var p=new Proxy(target,{ + getOwnPropertyDescriptor(t,k){log.push('get:'+k);return Reflect.getOwnPropertyDescriptor(t,k)}, + defineProperty(t,k,d){log.push('define:'+k);return Reflect.defineProperty(t,k,d)} + }); + var accepted=Reflect.set({x:1},'x',42,p); + var frozen=new Proxy(Object.preventExtensions({}),{}); + var a=[];Object.defineProperty(a,'length',{writable:false}); + var ap=new Proxy(a,{}); + var rejected=!Reflect.set(frozen,'x',1)&&!Reflect.set(ap,'0',1); + var token={}; var threw=false; + var throwing=new Proxy({}, {defineProperty(){throw token}}); + try{Reflect.set({x:0},'x',1,throwing)}catch(e){threw=e===token} + accepted&&target.x===42&&log.join(',')==='get:x,define:x'&&rejected&&threw; + "#, + ); +} + +#[test] +fn ordinary_property_accessor_reentry_and_receiver_rules() { + check( + r#" + var calls=0;var o={}; + Object.defineProperty(o,'x',{configurable:true,get(){ + delete this.x; this.y=9; return 7; + },set(v){calls++;delete this.x;this.x=v}}); + var read=o.x; + Object.defineProperty(o,'x',{configurable:true,set(v){calls++;delete this.x;this.x=v}}); + o.x=42; + var r={set x(v){calls+=100}}; + var rejected=!Reflect.set({x:0},'x',1,r); + var receiver={};var p={set q(v){this.saved=v}}; + var ok=Reflect.set(p,'q',12,receiver); + read===7&&o.x===42&&calls===1&&rejected&&ok&&receiver.saved===12; + "#, + ); +} + +#[test] +fn ordinary_property_update_relocates_after_conversion() { + check( + r#" + var o={a:1,x:{valueOf(){delete o.a;delete o.x;o.y=7;return 4}},z:3}; + o.x++; + var first=o.x===5&&o.y===7&&o.z===3; + o.x={valueOf(){Object.defineProperty(o,'x',{value:9,writable:false});return 2}}; + var accepted=false;try{(function(){'use strict';o.x+=1})()}catch(e){accepted=e instanceof TypeError} + first&&accepted&&o.x===9; + "#, + ); +} + +#[test] +fn ordinary_property_metadata_does_not_invoke_accessors_and_missing_stays_distinct() { + check( + r#" + var calls=0;var o={get x(){calls++;return undefined}}; + var a=Object.hasOwn(o,'x')&&o.propertyIsEnumerable('x'); + var d=Object.getOwnPropertyDescriptor(o,'x'); + var b=typeof d.get==='function'&&d.set===undefined&&calls===0; + var p=new Proxy({}, {get(){calls++;return undefined}}); + var c=Object.create(p);var v=c.unknown; + a&&b&&v===undefined&&calls===1&&o.x===undefined&&calls===2; + "#, + ); +} + +#[test] +fn ordinary_property_value_define_obeys_all_flags_and_same_value() { + check( + r#" + var values=[undefined,null,true,0,-0,1,1.5,NaN,'x',Symbol('s'),{},1n]; + var good=true; + for(var writable of [false,true])for(var configurable of [false,true])for(var enumerable of [false,true]){ + for(var old of values)for(var next of values){ + var o={};Object.defineProperty(o,'x',{value:old,writable,configurable,enumerable}); + Object.preventExtensions(o); + var expected=writable||configurable||Object.is(old,next); + var accepted=Reflect.defineProperty(o,'x',{value:next}); + var d=Object.getOwnPropertyDescriptor(o,'x'); + good=good&&accepted===expected&&Object.is(d.value,expected?next:old)&&d.writable===writable&&d.configurable===configurable&&d.enumerable===enumerable; + } + } + good; + "#, + ); +} + +#[test] +fn ordinary_property_dictionary_symbols_and_self_references() { + check( + r#" + var o={};var s=Symbol('key');var a=Symbol('a'),b=Symbol('b'); + for(var i=0;i<100;i++)o['p'+i]=i; + delete o.p1;delete o.p50; + o.p99=o;o[s]=a;o[s]=a;o[s]=b; + var keys=Reflect.ownKeys(o); + o.p2=42; + o.p99=null; + o.p2===42&&o[s]===b&&o.p99===null&&keys[keys.length-1]===s&&!Object.hasOwn(o,'p1'); + "#, + ); +} + +#[test] +fn ordinary_property_array_length_conversion_precedes_readonly_rejection() { + check( + r#" + var a=[];Object.defineProperty(a,'length',{writable:false}); + var calls=0;var rejected=!Reflect.set(a,'length',{valueOf(){calls++;return 0}}); + var token={};var threw=false; + try{Reflect.set(a,'length',{valueOf(){throw token}})}catch(e){threw=e===token} + rejected&&calls>0&&threw&&a.length===0; + "#, + ); +} diff --git a/src/engine/object/properties.rs b/src/engine/object/properties.rs index 75bf9dfa..8b9fd655 100644 --- a/src/engine/object/properties.rs +++ b/src/engine/object/properties.rs @@ -192,6 +192,12 @@ impl Runtime { ) -> Result, RuntimeError> { let _operation = self.operation(); self.validate_object_and_key(object, key)?; + if let Some(snapshot) = self.ordinary_property_snapshot(object, key)? { + return match snapshot { + Some(snapshot) => self.materialize_property_snapshot(object, key, snapshot), + None => Ok(None), + }; + } if self.typed_array_is_object(object)? && let Some(numeric) = self.typed_array_canonical_numeric_index(key)? { @@ -247,6 +253,15 @@ impl Runtime { } }; + self.materialize_property_snapshot(object, key, snapshot) + } + + pub(super) fn materialize_property_snapshot( + &self, + object: &ObjectRef, + key: &PropertyKey, + snapshot: PropertySnapshot, + ) -> Result, RuntimeError> { match snapshot { PropertySnapshot::Data { value, flags } => { Ok(Some(CompleteOrdinaryPropertyDescriptor::Data { @@ -1252,6 +1267,9 @@ impl Runtime { ) -> Result { let _operation = self.operation(); self.validate_object_and_key(object, key)?; + if let Some(flags) = self.ordinary_property_flags(object, key)? { + return Ok(flags.is_some()); + } if self.typed_array_is_object(object)? && let Some(numeric) = self.typed_array_canonical_numeric_index(key)? { @@ -1282,6 +1300,9 @@ impl Runtime { key: &PropertyKey, ) -> Result { self.validate_object_and_key(object, key)?; + if let Some(flags) = self.ordinary_property_flags(object, key)? { + return Ok(flags.is_some_and(|own| own.flags.enumerable)); + } if self.typed_array_is_object(object)? && let Some(numeric) = self.typed_array_canonical_numeric_index(key)? { From eaea10323dfdb8906aec6aa68f18aef2573d6746 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 14:26:22 +0800 Subject: [PATCH 05/14] perf(object): share value definitions and transactional slot updates --- .../checks/binary_object/evidence/coercion.py | 2 +- src/engine/heap/object_storage.rs | 35 ++++++- src/engine/heap/runtime/mod.rs | 23 +++++ src/engine/heap/tests/storage.rs | 99 +++++++++++++++++++ src/engine/object/ordinary_storage.rs | 54 ++++++++-- src/engine/object/private_elements.rs | 13 +-- src/engine/object/properties.rs | 17 +--- src/engine/object/property.rs | 26 ++++- src/engine/object/storage.rs | 12 +-- src/engine/value/collection_key.rs | 11 +++ 10 files changed, 239 insertions(+), 53 deletions(-) diff --git a/scripts/checks/binary_object/evidence/coercion.py b/scripts/checks/binary_object/evidence/coercion.py index b67ee43f..6e9e9d82 100644 --- a/scripts/checks/binary_object/evidence/coercion.py +++ b/scripts/checks/binary_object/evidence/coercion.py @@ -5,7 +5,7 @@ 'src/engine/code/binary_object/function_translate/mod.rs': 'ee4d423731977d088c45a0ee2f0fcf642b119f2c2dd4c6f9243d724a761545d7', 'src/engine/code/binary_object/ordinary_leaf.rs': 'b7553332908929126730993a655bbd55acbe34df3ae7acf7415486f0c3fd81a4', 'src/engine/code/binary_object_publish.rs': 'd7dd67291f5a8e7535ff7e7fb6802aa8348f5186c32235a8411ec79d6c5a0ae7', - 'src/engine/heap/runtime/tests.rs': '36ce3fafb354a8c6049bc098069437ea653eed726cd3748f6e00cf30ccacad96', + 'src/engine/heap/runtime/tests.rs': '3b7f56cc12f5b2a7cb068274abeb02e1543356a45cccef109eb7b7c0de1a9d0d', 'src/engine/code/binary_object/ordinary_leaf/tests.rs': '8a659586b5dfd17d5b3f1863913ddc952fd46fbb080bbf422133939fedb86630', 'src/engine/heap/runtime/tests/accessors.rs': '7e7aadee6f2248192a6c1438029e26e1153f9412e148cf8c672f7e7d97ddc1d4', 'src/engine/heap/runtime/tests/active_frames.rs': '609cbd20f1c0499710c9494e365fabfa94f3e8c19be359d4a721aff39bfde0bd', diff --git a/src/engine/heap/object_storage.rs b/src/engine/heap/object_storage.rs index 346a3e51..a626636d 100644 --- a/src/engine/heap/object_storage.rs +++ b/src/engine/heap/object_storage.rs @@ -1,5 +1,11 @@ use super::*; +/// The runtime may undo retained Atoms only before slot publication. +pub(crate) struct SlotReplacementError { + pub(crate) error: HeapError, + pub(crate) published: bool, +} + impl Heap { /// Read one live object record. pub fn object(&self, id: ObjectId) -> Result<&ObjectData, HeapError> { @@ -602,11 +608,34 @@ impl Heap { slot_index: usize, replacement: PropertySlot, ) -> Result { - self.validate_replacement_slot(id, slot_index, &replacement)?; - let new_edges = property_slot_edges(&replacement); - self.retain_edges_transactionally(&new_edges)?; + self.replace_object_slot_with_status(id, slot_index, replacement) + .map_err(|failure| failure.error) + } + pub(crate) fn replace_object_slot_with_status( + &mut self, + id: ObjectId, + slot_index: usize, + replacement: PropertySlot, + ) -> Result { + self.validate_replacement_slot(id, slot_index, &replacement) + .map_err(|error| SlotReplacementError { + error, + published: false, + })?; + let new_edges = property_slot_edges(&replacement); + self.retain_edges_transactionally(&new_edges) + .map_err(|error| SlotReplacementError { + error, + published: false, + })?; + // Validation and commit are adjacent under &mut Heap. There is no + // mutation/callback between them, so the validated slot cannot vanish. self.replace_retained_object_slot(id, slot_index, replacement) + .map_err(|error| SlotReplacementError { + error, + published: true, + }) } /// Commit a validated replacement after its edges have been retained. diff --git a/src/engine/heap/runtime/mod.rs b/src/engine/heap/runtime/mod.rs index 28108c57..efbb6475 100644 --- a/src/engine/heap/runtime/mod.rs +++ b/src/engine/heap/runtime/mod.rs @@ -416,6 +416,29 @@ impl RuntimeState { self.ensure_dictionary_layout(object) } + /// One slot transaction, including Atom ownership. Once published, a + /// cleanup failure cannot roll back the new slot's Atom references. + pub(crate) fn replace_property_slot( + &mut self, + object: ObjectId, + index: usize, + replacement: PropertySlot, + ) -> Result<(), RuntimeError> { + let atoms = self.retain_slot_atoms(std::slice::from_ref(&replacement))?; + match self + .heap + .replace_object_slot_with_status(object, index, replacement) + { + Ok(cleanup) => self.apply_cleanup(cleanup), + Err(failure) => { + if !failure.published { + self.release_atoms(atoms)?; + } + Err(failure.error.into()) + } + } + } + pub(crate) fn apply_cleanup(&mut self, cleanup: HeapCleanup) -> Result<(), RuntimeError> { self.unlink_finalized_shapes(cleanup.finalized_shape_ids); self.release_atoms(cleanup.atoms) diff --git a/src/engine/heap/tests/storage.rs b/src/engine/heap/tests/storage.rs index bc6eba37..52d563fb 100644 --- a/src/engine/heap/tests/storage.rs +++ b/src/engine/heap/tests/storage.rs @@ -1296,3 +1296,102 @@ fn shape_prototype_edge_participates_in_cycle_collection() { assert_eq!(stats.cleanup.finalized_shapes, 1); assert_eq!(heap.counts().live, 0); } + +#[test] +fn property_slot_transaction_retains_before_publish_and_marks_cleanup_failures() { + let mut heap = Heap::new(); + let shape = one_slot_shape(&mut heap); + let object = heap + .allocate_object(ObjectData::ordinary( + shape, + vec![PropertySlot::Data(RawValue::Int(7))], + )) + .unwrap(); + let target = heap + .allocate_object(ObjectData::ordinary( + shape, + vec![PropertySlot::Data(RawValue::Undefined)], + )) + .unwrap(); + heap.live_node_mut(RawId::Object(target)).unwrap().strong = u32::MAX; + let failure = heap + .replace_object_slot_with_status(object, 0, PropertySlot::Data(RawValue::Object(target))) + .err() + .unwrap(); + assert!(!failure.published); + assert!(matches!( + heap.object(object).unwrap().slots[0], + PropertySlot::Data(RawValue::Int(7)) + )); + assert_eq!(heap.object_strong_count(target), Ok(u32::MAX)); + heap.live_node_mut(RawId::Object(target)).unwrap().strong = 1; + heap.replace_object_slot(object, 0, PropertySlot::Data(RawValue::Object(target))) + .unwrap(); + heap.live_node_mut(RawId::Object(target)).unwrap().strong = 0; + let failure = heap + .replace_object_slot_with_status(object, 0, PropertySlot::Data(RawValue::Int(42))) + .err() + .unwrap(); + assert!(failure.published); + assert!(matches!( + heap.object(object).unwrap().slots[0], + PropertySlot::Data(RawValue::Int(42)) + )); + heap.live_node_mut(RawId::Object(target)).unwrap().strong = 1; + heap.release_object(target).unwrap(); + heap.release_object(object).unwrap(); + heap.release_shape(shape).unwrap(); + assert_eq!(heap.counts().live, 0); +} + +#[test] +fn property_slot_transaction_keeps_new_symbol_owned_after_post_publish_failure() { + use crate::engine::api::runtime::Runtime; + use crate::engine::object::{DescriptorField, OrdinaryPropertyDescriptor}; + use crate::engine::value::Value; + let runtime = Runtime::new(); + let object = runtime.new_object(None).unwrap(); + let old = runtime.new_object(None).unwrap(); + let key = runtime.intern_property_key("x").unwrap(); + runtime + .define_own_property( + &object, + &key, + &OrdinaryPropertyDescriptor { + value: DescriptorField::Present(Value::Object(old.clone())), + writable: DescriptorField::Present(true), + ..OrdinaryPropertyDescriptor::new() + }, + ) + .unwrap(); + { + let mut state = runtime.0.state.borrow_mut(); + let symbol = state.atoms.new_symbol(Some("replacement")).unwrap(); + state + .heap + .live_node_mut(RawId::Object(old.object_id())) + .unwrap() + .strong = 0; + assert!( + state + .replace_property_slot( + object.object_id(), + 0, + PropertySlot::Data(RawValue::Symbol(symbol)) + ) + .is_err() + ); + assert_eq!(state.atoms.resolve(symbol).unwrap().ref_count, Some(2)); + assert!( + matches!(state.heap.object(object.object_id()).unwrap().slots[0], PropertySlot::Data(RawValue::Symbol(atom)) if atom == symbol) + ); + state + .heap + .live_node_mut(RawId::Object(old.object_id())) + .unwrap() + .strong = 1; + state.atoms.release(symbol).unwrap(); + } + drop(object); + drop(old); +} diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index 3065ce7e..f833ea86 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -116,14 +116,7 @@ fn replace_data( slot: OwnSlot, replacement: PropertySlot, ) -> Result<(), RuntimeError> { - let atoms = state.retain_slot_atoms(std::slice::from_ref(&replacement))?; - match state.heap.replace_object_slot(id, slot.index, replacement) { - Ok(cleanup) => state.apply_cleanup(cleanup), - Err(error) => { - state.release_atoms(atoms)?; - Err(error.into()) - } - } + state.replace_property_slot(id, slot.index, replacement) } pub(super) enum ReadProbe { @@ -223,6 +216,51 @@ impl Runtime { } } +impl Runtime { + pub(super) fn try_define_ordinary_value( + &self, + object: &ObjectRef, + key: &PropertyKey, + descriptor: &crate::engine::object::OrdinaryPropertyDescriptor, + ) -> Result, RuntimeError> { + use crate::engine::object::DescriptorField; + let DescriptorField::Present(value) = &descriptor.value else { + return Ok(None); + }; + if !matches!(descriptor.writable, DescriptorField::Absent) + || !matches!(descriptor.enumerable, DescriptorField::Absent) + || !matches!(descriptor.configurable, DescriptorField::Absent) + || !matches!(descriptor.get, DescriptorField::Absent) + || !matches!(descriptor.set, DescriptorField::Absent) + { + return Ok(None); + } + let mut state = self.0.state.borrow_mut(); + let id = object.object_id(); + if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + return Ok(None); + } + let Some(slot) = locate(&state, id, key.atom())? else { + return Ok(None); + }; + let PropertySlot::Data(old) = &state.heap.object(id)?.slots[slot.index] else { + return Ok(None); + }; + let raw = self.raw_property_value(value)?; + if !crate::engine::object::property::data_value_update_allowed( + slot.flags.configurable, + slot.flags.writable, + old, + &raw, + crate::engine::value::collection_key::same_value, + ) { + return Ok(Some(false)); + } + replace_data(&mut state, id, slot, PropertySlot::Data(raw))?; + Ok(Some(true)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/engine/object/private_elements.rs b/src/engine/object/private_elements.rs index 078ff995..f2c52c9e 100644 --- a/src/engine/object/private_elements.rs +++ b/src/engine/object/private_elements.rs @@ -181,18 +181,7 @@ impl Runtime { let replacement = PropertySlot::Data(raw); let mut state = self.0.state.borrow_mut(); - let retained_atoms = state.retain_slot_atoms(std::slice::from_ref(&replacement))?; - let cleanup = match state - .heap - .replace_object_slot(object_id, index, replacement) - { - Ok(cleanup) => cleanup, - Err(error) => { - state.release_atoms(retained_atoms)?; - return Err(error.into()); - } - }; - state.apply_cleanup(cleanup)?; + state.replace_property_slot(object_id, index, replacement)?; drop(state); // `replace_object_slot` retained the heap occurrence before this // incoming public root is released. diff --git a/src/engine/object/properties.rs b/src/engine/object/properties.rs index 8b9fd655..30c90489 100644 --- a/src/engine/object/properties.rs +++ b/src/engine/object/properties.rs @@ -443,19 +443,7 @@ impl Runtime { }; let raw = self.raw_property_value(&initialized)?; let mut state = self.0.state.borrow_mut(); - let retained_atoms = state.retain_slot_atoms(&[PropertySlot::Data(raw.clone())])?; - let cleanup = - match state - .heap - .replace_object_slot(object_id, slot_index, PropertySlot::Data(raw)) - { - Ok(cleanup) => cleanup, - Err(error) => { - state.release_atoms(retained_atoms)?; - return Err(error.into()); - } - }; - state.apply_cleanup(cleanup)?; + state.replace_property_slot(object_id, slot_index, PropertySlot::Data(raw))?; drop(state); drop(initialized); Ok(()) @@ -576,6 +564,9 @@ impl Runtime { let _operation = self.operation(); self.validate_object_and_key(object, key)?; self.validate_descriptor_domains(descriptor)?; + if let Some(defined) = self.try_define_ordinary_value(object, key, descriptor)? { + return Ok(PropertyDefineOutcome::Defined(defined)); + } if descriptor.is_mixed_descriptor() { return Err(PropertyDefinitionError::InvalidDescriptor.into()); } diff --git a/src/engine/object/property.rs b/src/engine/object/property.rs index 2ef730c6..36a847cd 100644 --- a/src/engine/object/property.rs +++ b/src/engine/object/property.rs @@ -123,6 +123,18 @@ impl CompletePropertyDescriptor { } } +/// Shared Data [[Value]] update permission. Set does not use this rule: +/// assignment to a non-writable property is rejected even for SameValue. +pub(crate) fn data_value_update_allowed( + configurable: bool, + writable: bool, + old: &V, + new: &V, + same_value: impl FnOnce(&V, &V) -> bool, +) -> bool { + configurable || writable || same_value(new, old) +} + /// Why an ordinary property definition was rejected. /// /// ECMAScript exposes these rejections as `false` or a `TypeError`, depending @@ -269,11 +281,15 @@ where if descriptor.writable == Some(true) { return Err(PropertyDefinitionError::WritableOnNonWritable); } - if descriptor - .value - .as_ref() - .is_some_and(|new_value| !same_value(new_value, value)) - { + if descriptor.value.as_ref().is_some_and(|new_value| { + !data_value_update_allowed( + current_configurable, + *writable, + value, + new_value, + &same_value, + ) + }) { return Err(PropertyDefinitionError::ValueOnNonWritable); } } diff --git a/src/engine/object/storage.rs b/src/engine/object/storage.rs index d9fb4fd2..aa9dc894 100644 --- a/src/engine/object/storage.rs +++ b/src/engine/object/storage.rs @@ -384,17 +384,7 @@ impl Runtime { // borrowing the shape, before cloning entries or unrelated value slots. if let Some((index, existing_flags)) = existing { if existing_flags == flags { - let retained_atoms = state.retain_slot_atoms(std::slice::from_ref(&replacement))?; - return match state - .heap - .replace_object_slot(object_id, index, replacement) - { - Ok(cleanup) => state.apply_cleanup(cleanup), - Err(error) => { - state.release_atoms(retained_atoms)?; - Err(error.into()) - } - }; + return state.replace_property_slot(object_id, index, replacement); } } if let Some((index, _)) = existing { diff --git a/src/engine/value/collection_key.rs b/src/engine/value/collection_key.rs index 3ede0e9b..4a1483ab 100644 --- a/src/engine/value/collection_key.rs +++ b/src/engine/value/collection_key.rs @@ -29,6 +29,17 @@ pub(crate) fn same_value_zero(left: &RawValue, right: &RawValue) -> bool { } } +/// SameValue shares the non-coercing key equality rules, but distinguishes +/// the two zero signs. Inputs have already passed runtime-domain validation. +pub(crate) fn same_value(left: &RawValue, right: &RawValue) -> bool { + if let (Some(left), Some(right)) = (number(left), number(right)) { + if left == 0.0 && right == 0.0 { + return left.is_sign_negative() == right.is_sign_negative(); + } + } + same_value_zero(left, right) +} + pub(crate) fn hash(key: &RawValue, state: &mut H) { match key { RawValue::Undefined => state.write_u8(0), From bc16d9790b5431991d15057f3675de9f641c9526 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 14:37:57 +0800 Subject: [PATCH 06/14] perf(object): finish ordinary property dispatch and enforce kernel boundaries --- docs/ordinary-property-plan.md | 6 +- .../rules/ordinary_properties.py | 42 +++++ scripts/checks/binary_object/scan.py | 3 +- .../tests/test_ordinary_properties.py | 47 ++++++ src/engine/object/README.md | 8 +- src/engine/object/internal_methods.rs | 145 ++++++++++++------ src/engine/object/ordinary.rs | 34 ++-- src/engine/object/ordinary_storage.rs | 70 ++++++--- src/engine/object/ordinary_tests.rs | 81 ++++++++++ 9 files changed, 336 insertions(+), 100 deletions(-) create mode 100644 scripts/checks/binary_object/rules/ordinary_properties.py create mode 100644 scripts/checks/binary_object/tests/test_ordinary_properties.py diff --git a/docs/ordinary-property-plan.md b/docs/ordinary-property-plan.md index 234cf7e8..23565799 100644 --- a/docs/ordinary-property-plan.md +++ b/docs/ordinary-property-plan.md @@ -1,6 +1,6 @@ # 普通对象属性访问内核改造计划 -状态:设计草案,尚未实现、尚未测量。源码核对基线 `fec7519`(2026-09-11)。 +状态:P0–P4 已实现,P5 收口与完整验收进行中。源码核对基线 `fec7519`(2026-09-11)。 面向实施者和评审者:按本文迁移普通属性读、写、定义及元数据查询,并验证语义、所有权和实际性能。 来源为 benchmark issue #16 的对象属性问题;历史热点仅用于选方向,不能作为当前收益预测。 @@ -194,4 +194,6 @@ Test262 fingerprint 变化与实际行为变化分别报告,不能改冻结 re 每个切片记录源码提交、修改拥有者、已运行验证、未运行验证、基线/新 ELF 及工作负载哈希、A/B 分布、 退化和最终处置。原始实验保留在忽略的 target;计划和维护契约入库,不预填结果。 涉及借用/引用事务及 Set 语义收敛的变更需要独立评审,实施者自查不得称为独立评审。 -本文当前仅完成源码驱动的设计,P0–P5 均未实施。 +实现记录:P1 `88a39d1`,P2 `5ad6436`,P3 `68ce00f`,P4 `eaea103`。P5 正在完成特殊分派收口、架构反例和完整验收。 +存储实现采用私有 `OwnSlot` 与短借用 probe,不导出 session/slot;白名单同时检查 kind 和 payload。 +引用事务由 RuntimeState 统一管理,发布前失败回滚新引用,发布后清理失败不撤销已发布引用。 diff --git a/scripts/checks/binary_object/rules/ordinary_properties.py b/scripts/checks/binary_object/rules/ordinary_properties.py new file mode 100644 index 00000000..cb73743b --- /dev/null +++ b/scripts/checks/binary_object/rules/ordinary_properties.py @@ -0,0 +1,42 @@ +"""Property kernel ownership guards; behavioral coverage remains in Rust/JS.""" +import re + +FILES = ( + "src/engine/object/ordinary_storage.rs", + "src/engine/object/ordinary.rs", + "src/engine/object/internal_methods.rs", + "src/engine/heap/runtime/mod.rs", + "src/engine/heap/object_storage.rs", +) + + +def check(ctx): + if getattr(ctx, "self_test_marker_authorized", False): + # The existing codec isolation fixtures deliberately omit the property + # engine. Dedicated mutation tests exercise these guards on real files. + return + sources = [] + for relative in FILES: + path = ctx.root / relative + if path.is_symlink() or not path.is_file(): + ctx.fail("ordinary-property-source", f"missing regular source: {relative}") + return + sources.append(ctx.rust_code_only(path.read_text())) + storage, ordinary, dispatch, runtime, heap = sources + compact = lambda text: re.sub(r"\s+", "", text) + requirements = [ + (not re.search(r"pub(?:\([^)]*\))?\s+struct\s+OwnSlot", storage), "slot positions must remain private to the storage owner"), + ("(ObjectKind::Ordinary,ObjectPayload::Ordinary)" in compact(storage), "ordinary eligibility must include the semantic class"), + (not re.search(r"\.(?:call_internal|internal_set|materialize_auto_init_property)\s*\(", storage), "storage must not execute callbacks or observable internal methods"), + ("ordinary_set_fast_path_available" not in ordinary + dispatch, "ordinary Set must not pre-scan the prototype chain"), + ("self.validate_object_and_key(object,key)?" in compact(ordinary) and "self.validate_value_domain(&value," in compact(ordinary) and "self.validate_value_domain(&receiver," in compact(ordinary), "Set must validate object, key, value and receiver domains"), + ("rejected_object.as_ref().unwrap_or(receiver)" in compact(ordinary), "Proxy forwarding diagnostics must use the rejected target"), + ("if!failure.published{self.release_atoms(atoms)?;}" in compact(runtime), "only pre-publication failures may roll back replacement Atoms"), + ] + body, _, _ = ctx.unique_braced_item(heap, re.compile(r"fn\s+replace_object_slot_with_status\s*\([^{}]*\)\s*->[^{}]*\{"), "ordinary-property-transaction", "slot replacement") + retained = body.find("retain_edges_transactionally") + published = body.find("replace_retained_object_slot") + requirements.append((0 <= retained < published, "replacement edges must be retained before publication")) + for accepted, message in requirements: + if not accepted: + ctx.fail("ordinary-property-contract", message) diff --git a/scripts/checks/binary_object/scan.py b/scripts/checks/binary_object/scan.py index 46077727..13dd00af 100644 --- a/scripts/checks/binary_object/scan.py +++ b/scripts/checks/binary_object/scan.py @@ -1,6 +1,6 @@ """Ordered rule pipeline. Later rules consume earlier authenticated observations.""" from .context import ScanContext -from .rules import source_setup, surface, native_plan, translation, ordinary_leaf, scalar, publication, runtime_protocols, runtime_contracts, wire_evidence, exception_evidence, coercion, coercion_evidence, reference_oracle, receipts, source_ownership, shared_transport +from .rules import source_setup, surface, native_plan, translation, ordinary_leaf, scalar, publication, runtime_protocols, runtime_contracts, wire_evidence, exception_evidence, coercion, coercion_evidence, reference_oracle, receipts, source_ownership, shared_transport, ordinary_properties RULES = ( ("source_setup", source_setup.check), @@ -20,6 +20,7 @@ ("receipts", receipts.check), ("source_ownership", source_ownership.check), ("shared_transport", shared_transport.check), + ("ordinary_properties", ordinary_properties.check), ) diff --git a/scripts/checks/binary_object/tests/test_ordinary_properties.py b/scripts/checks/binary_object/tests/test_ordinary_properties.py new file mode 100644 index 00000000..8abcb0db --- /dev/null +++ b/scripts/checks/binary_object/tests/test_ordinary_properties.py @@ -0,0 +1,47 @@ +from pathlib import Path +import tempfile +import unittest + +from binary_object.context import ScanContext +from binary_object.rules import ordinary_properties, source_setup + +ROOT = Path(__file__).resolve().parents[4] + + +class OrdinaryPropertyContracts(unittest.TestCase): + def scan(self, edits=()): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in ordinary_properties.FILES: + source = (ROOT / relative).read_text() + for path, before, after in edits: + if path == relative: + self.assertIn(before, source) + source = source.replace(before, after) + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(source) + context = ScanContext(root) + source_setup.check(context) + ordinary_properties.check(context) + return context.errors + + def test_current_contracts(self): + self.assertEqual(self.scan(), []) + + def test_bad_boundaries_are_rejected(self): + storage, ordinary, dispatch, runtime, heap = ordinary_properties.FILES + mutations = [ + (storage, "struct OwnSlot", "pub(crate) struct OwnSlot"), + # Replace every occurrence to simulate removal of the shared class gate. + (storage, "ObjectKind::Ordinary", "ObjectKind::ModuleNamespace"), + (storage, "fn locate(", "fn bad() { self.call_internal(); } fn locate("), + (dispatch, "impl Runtime {", "fn ordinary_set_fast_path_available() {} impl Runtime {"), + (ordinary, "self.validate_value_domain(&value,", "self.skip_domain(&value,"), + (ordinary, "rejected_object.as_ref().unwrap_or(receiver)", "receiver"), + (runtime, "if !failure.published", "if failure.published"), + (heap, ".retain_edges_transactionally(&new_edges)", ".skip_retain(&new_edges)"), + ] + for mutation in mutations: + with self.subTest(mutation=mutation): + self.assertTrue(self.scan([mutation])) diff --git a/src/engine/object/README.md b/src/engine/object/README.md index 132deee5..bba50c34 100644 --- a/src/engine/object/README.md +++ b/src/engine/object/README.md @@ -4,8 +4,9 @@ 通过 heap 保存原始记录与引用边;调用和用户代码执行交给 VM 协调;内置方法归 builtins。 -普通属性读写与定义的后续重构见[普通对象属性访问内核改造计划](../../../docs/ordinary-property-plan.md)。 -该文档为尚未实施的设计,包含目标职责、迁移步骤和验收条件。 +普通属性读写与定义的职责和验收见[普通对象属性访问内核改造计划](../../../docs/ordinary-property-plan.md)。 +`ordinary_storage` 在一次借用内完成分类、定位与槽更新;`ordinary` 拥有普通对象算法, +`internal_methods` 拥有特殊对象和 completion 分发。槽索引不跨回调逃逸;只 root 当前操作需要的值。 Dictionary 模式用于普通对象和已转为慢表示的 Array。共享 shape 首次分离,独占 shape 原地转换并脱离 weak cache。物理槽用 swap-remove,插入顺序由独立双向链接维护; @@ -35,6 +36,9 @@ Array 的中间删除/特殊描述符只在首次转换时移动 dense values; - [mod.rs](mod.rs):模块入口、共享接口与子模块声明。 - [object_literal.rs](object_literal.rs):Object-literal method publication.。 - [operations.rs](operations.rs):对象操作的中间状态与描述符转换。 +- [ordinary.rs](ordinary.rs):普通属性 Set、Get 与按需原型遍历。 +- [ordinary_storage.rs](ordinary_storage.rs):普通对象白名单、属性元数据与私有槽定位,复用 runtime 引用事务。 +- [ordinary_tests.rs](ordinary_tests.rs):普通属性访问的可观察语义与生命周期回归。 - [private_elements.rs](private_elements.rs):Runtime substrate for class private data fields.。 - [properties.rs](properties.rs):Runtime property lookup, definition, and object-layout operations.。 - [property.rs](property.rs):Ordinary ECMAScript property descriptors.。 diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 55532f34..740ed755 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -532,6 +532,9 @@ impl Runtime { object: &ObjectRef, key: &PropertyKey, ) -> Result, RuntimeError> { + if let Some(flags) = self.ordinary_property_flags(object, key)? { + return Ok(NativeConversion::Value(flags.is_some())); + } if self.proxy_snapshot_if_any(object)?.is_none() && !self.is_module_namespace_object(object)? { @@ -586,6 +589,11 @@ impl Runtime { object: &ObjectRef, key: &PropertyKey, ) -> Result, RuntimeError> { + if let Some(flags) = self.ordinary_property_flags(object, key)? { + return Ok(NativeConversion::Value( + flags.is_some_and(|own| own.flags.enumerable), + )); + } if self.proxy_snapshot_if_any(object)?.is_none() && !self.is_module_namespace_object(object)? { @@ -775,6 +783,27 @@ impl Runtime { realm: ContextId, object: &ObjectRef, key: &PropertyKey, + ) -> Result, RuntimeError> { + self.validate_object_and_key(object, key)?; + let mut prototype = None; + loop { + let current = prototype.as_ref().unwrap_or(object); + match self.ordinary_property_flags(current, key)? { + Some(Some(_)) => return Ok(NativeConversion::Value(true)), + Some(None) => match self.get_prototype_of(current)? { + Some(next) => prototype = Some(next), + None => return Ok(NativeConversion::Value(false)), + }, + None => return self.has_special_property(realm, current, key), + } + } + } + + fn has_special_property( + &self, + realm: ContextId, + object: &ObjectRef, + key: &PropertyKey, ) -> Result, RuntimeError> { if self.proxy_snapshot_if_any(object)?.is_some() { return self.proxy_has_property(realm, object, key); @@ -873,6 +902,7 @@ impl Runtime { key: &PropertyKey, receiver: Value, ) -> Result>, RuntimeError> { + let _operation = self.operation(); self.validate_object_and_key(object, key)?; self.validate_value_domain(&receiver, "property receiver")?; self.get_ordinary_chain(realm, object, key, receiver) @@ -1000,57 +1030,6 @@ impl Runtime { receiver: Value, ) -> Result, RuntimeError> { let _operation = self.operation(); - self.validate_object_and_key(object, key)?; - self.validate_value_domain(&value, "property value")?; - self.validate_value_domain(&receiver, "property receiver")?; - if self.proxy_snapshot_if_any(object)?.is_some() { - return self.proxy_set(realm, object, key, value, receiver); - } - if self.is_module_namespace_object(object)? { - return Ok(NativeConversion::Value(InternalSetResult::Rejected( - PropertySetRejection::ReadOnly, - ))); - } - if self.typed_array_is_object(object)? - && let Some(numeric) = self.typed_array_canonical_numeric_index(key)? - { - let receiver_is_target = - matches!(&receiver, Value::Object(receiver) if receiver == object); - match numeric { - CanonicalNumericIndex::Valid(index) if receiver_is_target => { - return Ok( - match self.typed_array_set_index(realm, object, index, &value)? { - NativeConversion::Value(()) => { - NativeConversion::Value(InternalSetResult::Accepted) - } - NativeConversion::Throw(value) => NativeConversion::Throw(value), - }, - ); - } - CanonicalNumericIndex::Valid(index) => { - if self - .typed_array_get_index_descriptor(object, index)? - .is_none() - { - return Ok(NativeConversion::Value(InternalSetResult::Accepted)); - } - } - CanonicalNumericIndex::Invalid if receiver_is_target => { - let element = self.typed_array_snapshot(object)?.element; - return Ok( - match self.typed_array_convert_element(realm, element, &value)? { - NativeConversion::Value(_) => { - NativeConversion::Value(InternalSetResult::Accepted) - } - NativeConversion::Throw(value) => NativeConversion::Throw(value), - }, - ); - } - CanonicalNumericIndex::Invalid => { - return Ok(NativeConversion::Value(InternalSetResult::Accepted)); - } - } - } match self.prepare_set_property_with_receiver_in_realm( Some(realm), object, @@ -1077,6 +1056,70 @@ impl Runtime { } } + /// Only encountered special targets reach this dispatch. Ordinary own + /// writes do not pre-classify or walk any prototype here. + pub(super) fn try_special_set( + &self, + realm: ContextId, + object: &ObjectRef, + key: &PropertyKey, + value: &Value, + receiver: &Value, + ) -> Result>, RuntimeError> { + let (proxy, namespace, typed) = { + let state = self.0.state.borrow(); + let data = state.heap.object(object.object_id())?; + ( + matches!(data.payload, ObjectPayload::Proxy(_)), + data.kind == crate::engine::heap::ObjectKind::ModuleNamespace, + matches!(data.payload, ObjectPayload::TypedArray(_)), + ) + }; + if proxy { + return self + .proxy_set(realm, object, key, value.clone(), receiver.clone()) + .map(Some); + } + if namespace { + return Ok(Some(NativeConversion::Value(InternalSetResult::Rejected( + PropertySetRejection::ReadOnly, + )))); + } + if !typed { + return Ok(None); + } + let Some(numeric) = self.typed_array_canonical_numeric_index(key)? else { + return Ok(None); + }; + let same_receiver = matches!(receiver, Value::Object(receiver) if receiver == object); + let result = match numeric { + CanonicalNumericIndex::Valid(index) if same_receiver => { + self.typed_array_set_index(realm, object, index, value)? + } + CanonicalNumericIndex::Invalid if same_receiver => { + let element = self.typed_array_snapshot(object)?.element; + match self.typed_array_convert_element(realm, element, value)? { + NativeConversion::Value(_) => NativeConversion::Value(()), + NativeConversion::Throw(value) => NativeConversion::Throw(value), + } + } + CanonicalNumericIndex::Invalid => NativeConversion::Value(()), + CanonicalNumericIndex::Valid(index) => { + if self + .typed_array_get_index_descriptor(object, index)? + .is_some() + { + return Ok(None); + } + NativeConversion::Value(()) + } + }; + Ok(Some(match result { + NativeConversion::Value(()) => NativeConversion::Value(InternalSetResult::Accepted), + NativeConversion::Throw(value) => NativeConversion::Throw(value), + })) + } + fn proxy_set( &self, realm: ContextId, diff --git a/src/engine/object/ordinary.rs b/src/engine/object/ordinary.rs index 511e720e..52208418 100644 --- a/src/engine/object/ordinary.rs +++ b/src/engine/object/ordinary.rs @@ -63,7 +63,6 @@ impl Runtime { return Err(RuntimeError::Invariant("exotic Set requires a realm")); } let mut cursor = Some(object.clone()); - let mut first = true; while let Some(current) = cursor { let same_receiver = matches!(&receiver, Value::Object(target) if target == ¤t); match self.ordinary_set_probe(¤t, key, &value, same_receiver)? { @@ -78,7 +77,9 @@ impl Runtime { SetProbe::Setter(set) => { return Ok(match set { Some(setter) => PropertySetAction::Call { - setter, + setter: crate::engine::object::CallableRef::from_validated_object( + ObjectRef::from_borrowed_handle(self.clone(), setter)?, + ), receiver, argument: value, }, @@ -87,22 +88,17 @@ impl Runtime { } SetProbe::Missing(prototype) => { cursor = prototype; - first = false; continue; } SetProbe::Special => { - // Only an encountered exotic prototype delegates. The initial - // object's special Set has already been selected by internal_set. - if !first - && (self.is_proxy_object(¤t)? - || self.typed_array_is_object(¤t)? - || self.is_module_namespace_object(¤t)?) + if realm.is_none() && self.is_proxy_object(¤t)? { + return Err(RuntimeError::Invariant("exotic Set requires a realm")); + } + if let Some(realm) = realm + && let Some(result) = + self.try_special_set(realm, ¤t, key, &value, &receiver)? { - let realm = - realm.ok_or(RuntimeError::Invariant("exotic Set requires a realm"))?; - return Ok(set_completion( - self.internal_set(realm, ¤t, key, value, receiver)?, - )); + return Ok(set_completion(result)); } } } @@ -132,7 +128,6 @@ impl Runtime { } } cursor = self.get_prototype_of(¤t)?; - first = false; } let Value::Object(receiver) = receiver else { return Ok(PropertySetAction::Rejected(PropertySetRejection::NotObject)); @@ -273,9 +268,10 @@ impl Runtime { ) -> Result>, RuntimeError> { use crate::engine::object::ordinary_storage::ReadProbe; use crate::engine::vm::Completion; - let mut current = object.clone(); + let mut prototype = None; loop { - match self.ordinary_read_probe(¤t, key)? { + let current = prototype.as_ref().unwrap_or(object); + match self.ordinary_read_probe(current, key)? { ReadProbe::Value(value) => return Ok(NativeConversion::Value(Some(value))), ReadProbe::Getter(None) => { return Ok(NativeConversion::Value(Some(Value::Undefined))); @@ -286,10 +282,10 @@ impl Runtime { Completion::Throw(value) => NativeConversion::Throw(value), }); } - ReadProbe::Missing(Some(prototype)) => current = prototype, + ReadProbe::Missing(Some(next)) => prototype = Some(next), ReadProbe::Missing(None) => return Ok(NativeConversion::Value(None)), ReadProbe::Special => { - return self.get_special_or_missing(realm, ¤t, key, receiver); + return self.get_special_or_missing(realm, current, key, receiver); } } } diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index f833ea86..dad9ae4c 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -4,7 +4,7 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; use crate::engine::atom::Atom; use crate::engine::heap::runtime::RuntimeState; -use crate::engine::heap::{ObjectId, ObjectPayload, PropertySlot}; +use crate::engine::heap::{ObjectId, ObjectKind, ObjectPayload, PropertySlot}; use crate::engine::object::shape::PropertyFlags; use crate::engine::object::{ObjectRef, PropertyKey}; use crate::engine::value::Value; @@ -14,6 +14,14 @@ struct OwnSlot { flags: PropertyFlags, } +// Payload alone is insufficient: module namespaces share Ordinary storage. +fn is_ordinary(data: &crate::engine::heap::ObjectData) -> bool { + matches!( + (data.kind, &data.payload), + (ObjectKind::Ordinary, ObjectPayload::Ordinary) + ) +} + // The caller keeps the state borrowed until the located slot is consumed. fn locate( state: &RuntimeState, @@ -43,7 +51,7 @@ fn locate( pub(super) enum SetProbe { Stored(bool), Writable, - Setter(Option), + Setter(Option), Missing(Option), Special, } @@ -66,7 +74,7 @@ impl Runtime { let selected = { let mut state = self.0.state.borrow_mut(); let id = object.object_id(); - if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + if !is_ordinary(state.heap.object(id)?) { return Ok(SetProbe::Special); } match locate(&state, id, key.atom())? { @@ -94,13 +102,7 @@ impl Runtime { } }; Ok(match selected { - Selected::Setter(set) => SetProbe::Setter( - set.map(|id| { - ObjectRef::from_borrowed_handle(self.clone(), id) - .map(crate::engine::object::CallableRef::from_validated_object) - }) - .transpose()?, - ), + Selected::Setter(set) => SetProbe::Setter(set), Selected::Missing(prototype) => SetProbe::Missing( prototype .map(|id| ObjectRef::from_borrowed_handle(self.clone(), id)) @@ -140,7 +142,7 @@ impl Runtime { self.validate_object_and_key(object, key)?; let state = self.0.state.borrow(); let id = object.object_id(); - if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + if !is_ordinary(state.heap.object(id)?) { return Ok(None); } Ok(Some(locate(&state, id, key.atom())?.map(|slot| OwnFlags { @@ -161,7 +163,7 @@ impl Runtime { use crate::engine::object::operations::PropertySnapshot; let state = self.0.state.borrow(); let id = object.object_id(); - if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + if !is_ordinary(state.heap.object(id)?) { return Ok(None); } let Some(slot) = locate(&state, id, key.atom())? else { @@ -193,25 +195,43 @@ impl Runtime { object: &ObjectRef, key: &PropertyKey, ) -> Result { - use crate::engine::object::operations::PropertySnapshot; - let Some(snapshot) = self.ordinary_property_snapshot(object, key)? else { - return Ok(ReadProbe::Special); - }; - Ok(match snapshot { - Some(PropertySnapshot::Data { value, .. }) => { - ReadProbe::Value(self.root_raw_value(&value)?) + enum Selected { + Value(crate::engine::heap::RawValue), + Getter(Option), + Missing(Option), + } + let selected = { + let state = self.0.state.borrow(); + let id = object.object_id(); + let data = state.heap.object(id)?; + if !is_ordinary(data) { + return Ok(ReadProbe::Special); } - Some(PropertySnapshot::Accessor { get, .. }) => ReadProbe::Getter( + match locate(&state, id, key.atom())? { + None => Selected::Missing(state.heap.shape(data.shape)?.prototype()), + Some(slot) => match &data.slots[slot.index] { + PropertySlot::Data(value) => Selected::Value(value.clone()), + PropertySlot::Accessor { get, .. } => Selected::Getter(*get), + PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) => { + return Ok(ReadProbe::Special); + } + }, + } + }; + Ok(match selected { + Selected::Value(value) => ReadProbe::Value(self.root_raw_value(&value)?), + Selected::Getter(get) => ReadProbe::Getter( get.map(|id| { ObjectRef::from_borrowed_handle(self.clone(), id) .map(crate::engine::object::CallableRef::from_validated_object) }) .transpose()?, ), - Some(PropertySnapshot::AutoInit | PropertySnapshot::VarRef { .. }) => { - ReadProbe::Special - } - None => ReadProbe::Missing(self.get_prototype_of(object)?), + Selected::Missing(prototype) => ReadProbe::Missing( + prototype + .map(|id| ObjectRef::from_borrowed_handle(self.clone(), id)) + .transpose()?, + ), }) } } @@ -237,7 +257,7 @@ impl Runtime { } let mut state = self.0.state.borrow_mut(); let id = object.object_id(); - if !matches!(state.heap.object(id)?.payload, ObjectPayload::Ordinary) { + if !is_ordinary(state.heap.object(id)?) { return Ok(None); } let Some(slot) = locate(&state, id, key.atom())? else { diff --git a/src/engine/object/ordinary_tests.rs b/src/engine/object/ordinary_tests.rs index ba36cc28..052a9aa3 100644 --- a/src/engine/object/ordinary_tests.rs +++ b/src/engine/object/ordinary_tests.rs @@ -8,6 +8,22 @@ fn check(source: &str) { assert_eq!(context.eval(source).unwrap(), Value::Bool(true)); } +#[test] +fn ordinary_property_context_free_set_rejects_proxy_prototype() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let Value::Object(object) = context.eval("Object.create(new Proxy({}, {}))").unwrap() else { + panic!("expected object") + }; + let key = runtime.intern_property_key("x").unwrap(); + assert!(matches!( + runtime.prepare_set_property(&object, &key, Value::Int(1)), + Err(crate::engine::api::runtime_error::RuntimeError::Invariant( + "exotic Set requires a realm" + )) + )); +} + #[test] fn ordinary_property_receiver_proxy_preserves_traps_and_rejection_object() { check( @@ -128,3 +144,68 @@ fn ordinary_property_array_length_conversion_precedes_readonly_rejection() { "#, ); } + +#[test] +fn ordinary_property_proxy_forwarding_preserves_rejection_classification() { + use crate::engine::object::operations::{InternalSetResult, PropertySetRejection}; + use crate::engine::value::conversion::NativeConversion; + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + for (source, name, expected) in [ + ( + "new Proxy(Object.preventExtensions({}),{})", + "x", + PropertySetRejection::NotExtensible, + ), + ( + "var a=[];Object.defineProperty(a,'length',{writable:false});new Proxy(a,{})", + "0", + PropertySetRejection::ArrayLengthReadOnly, + ), + ] { + let Value::Object(proxy) = context.eval(source).unwrap() else { + panic!("expected Proxy") + }; + let key = runtime.intern_property_key(name).unwrap(); + let outcome = runtime + .internal_set( + context.realm, + &proxy, + &key, + Value::Int(1), + Value::Object(proxy.clone()), + ) + .unwrap(); + assert!( + matches!(outcome, NativeConversion::Value(InternalSetResult::Rejected(reason)) if reason == expected) + ); + } +} + +#[test] +fn ordinary_property_replacing_last_heap_edge_reclaims_old_object() { + use crate::engine::object::{DescriptorField, OrdinaryPropertyDescriptor}; + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let object = runtime.new_object(None).unwrap(); + let old = runtime.new_object(None).unwrap(); + let old_id = old.object_id(); + let key = runtime.intern_property_key("x").unwrap(); + runtime + .define_own_property( + &object, + &key, + &OrdinaryPropertyDescriptor { + value: DescriptorField::Present(Value::Object(old)), + writable: DescriptorField::Present(true), + ..OrdinaryPropertyDescriptor::new() + }, + ) + .unwrap(); + assert_eq!( + runtime.0.state.borrow().heap.object_strong_count(old_id), + Ok(1) + ); + context.set_property(&object, &key, Value::Null).unwrap(); + assert!(runtime.0.state.borrow().heap.object(old_id).is_err()); +} From 41ba53a8a118cd02ef5d4fa9b536c0b0e883072b Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 15:01:43 +0800 Subject: [PATCH 07/14] perf(object): reuse special classification and borrow initial Set targets --- src/engine/object/internal_methods.rs | 27 ++++++------------ src/engine/object/ordinary.rs | 40 +++++++++++++++------------ src/engine/object/ordinary_storage.rs | 33 +++++++++++++++++----- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 740ed755..037fdbf4 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -10,6 +10,7 @@ use crate::engine::api::error::{Error, ErrorKind, NativeErrorKind, NativeErrorMessage}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::object::ordinary_storage::SpecialKind; use crate::engine::atom::{Atom, PropertyKeyKind}; use crate::engine::builtins::CanonicalNumericIndex; @@ -910,18 +911,19 @@ impl Runtime { pub(super) fn get_special_or_missing( &self, + kind: SpecialKind, realm: ContextId, object: &ObjectRef, key: &PropertyKey, receiver: Value, ) -> Result>, RuntimeError> { - if self.proxy_snapshot_if_any(object)?.is_some() { + if matches!(kind, SpecialKind::Proxy) { return Ok(match self.proxy_get(realm, object, key, receiver)? { Completion::Return(value) => NativeConversion::Value(Some(value)), Completion::Throw(value) => NativeConversion::Throw(value), }); } - if self.typed_array_is_object(object)? + if matches!(kind, SpecialKind::TypedArray) && let Some(numeric) = self.typed_array_canonical_numeric_index(key)? { let value = match numeric { @@ -932,10 +934,7 @@ impl Runtime { }; return Ok(NativeConversion::Value(Some(value))); } - let own = match self.internal_get_own_property(realm, object, key)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), - }; + let own = self.get_own_property(object, key)?; if let Some(own) = own { return match own { CompleteOrdinaryPropertyDescriptor::Data { value, .. } => { @@ -1060,32 +1059,24 @@ impl Runtime { /// writes do not pre-classify or walk any prototype here. pub(super) fn try_special_set( &self, + kind: SpecialKind, realm: ContextId, object: &ObjectRef, key: &PropertyKey, value: &Value, receiver: &Value, ) -> Result>, RuntimeError> { - let (proxy, namespace, typed) = { - let state = self.0.state.borrow(); - let data = state.heap.object(object.object_id())?; - ( - matches!(data.payload, ObjectPayload::Proxy(_)), - data.kind == crate::engine::heap::ObjectKind::ModuleNamespace, - matches!(data.payload, ObjectPayload::TypedArray(_)), - ) - }; - if proxy { + if matches!(kind, SpecialKind::Proxy) { return self .proxy_set(realm, object, key, value.clone(), receiver.clone()) .map(Some); } - if namespace { + if matches!(kind, SpecialKind::ModuleNamespace) { return Ok(Some(NativeConversion::Value(InternalSetResult::Rejected( PropertySetRejection::ReadOnly, )))); } - if !typed { + if !matches!(kind, SpecialKind::TypedArray) { return Ok(None); } let Some(numeric) = self.typed_array_canonical_numeric_index(key)? else { diff --git a/src/engine/object/ordinary.rs b/src/engine/object/ordinary.rs index 52208418..c89db012 100644 --- a/src/engine/object/ordinary.rs +++ b/src/engine/object/ordinary.rs @@ -7,7 +7,7 @@ use crate::engine::object::operations::{ ArrayOwnKey, InternalDefineResult, InternalSetResult, PropertyDefineOutcome, PropertySetAction, PropertySetRejection, }; -use crate::engine::object::ordinary_storage::SetProbe; +use crate::engine::object::ordinary_storage::{SetProbe, SpecialKind}; use crate::engine::object::{ CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, @@ -62,10 +62,11 @@ impl Runtime { { return Err(RuntimeError::Invariant("exotic Set requires a realm")); } - let mut cursor = Some(object.clone()); - while let Some(current) = cursor { - let same_receiver = matches!(&receiver, Value::Object(target) if target == ¤t); - match self.ordinary_set_probe(¤t, key, &value, same_receiver)? { + let mut prototype = None; + loop { + let current = prototype.as_ref().unwrap_or(object); + let same_receiver = matches!(&receiver, Value::Object(target) if target == current); + match self.ordinary_set_probe(current, key, &value, same_receiver)? { SetProbe::Stored(accepted) => { return Ok(if accepted { PropertySetAction::Complete @@ -86,29 +87,29 @@ impl Runtime { None => PropertySetAction::Rejected(PropertySetRejection::NoSetter), }); } - SetProbe::Missing(prototype) => { - cursor = prototype; + SetProbe::Missing(next) => { + let Some(next) = next else { break }; + prototype = Some(next); continue; } - SetProbe::Special => { - if realm.is_none() && self.is_proxy_object(¤t)? { + SetProbe::Special(kind) => { + if realm.is_none() && matches!(kind, SpecialKind::Proxy) { return Err(RuntimeError::Invariant("exotic Set requires a realm")); } if let Some(realm) = realm && let Some(result) = - self.try_special_set(realm, ¤t, key, &value, &receiver)? + self.try_special_set(kind, realm, current, key, &value, &receiver)? { return Ok(set_completion(result)); } } } - if let Some(property) = self.get_own_property(¤t, key)? { + if let Some(property) = self.get_own_property(current, key)? { match property { CompleteOrdinaryPropertyDescriptor::Data { writable, .. } => { - if same_receiver - && self.array_own_key(¤t, key)? == ArrayOwnKey::Length + if same_receiver && self.array_own_key(current, key)? == ArrayOwnKey::Length { - return self.prepare_set_array_length(realm, ¤t, key, value); + return self.prepare_set_array_length(realm, current, key, value); } if !writable { return Ok(PropertySetAction::Rejected(PropertySetRejection::ReadOnly)); @@ -127,7 +128,10 @@ impl Runtime { } } } - cursor = self.get_prototype_of(¤t)?; + let Some(next) = self.get_prototype_of(current)? else { + break; + }; + prototype = Some(next); } let Value::Object(receiver) = receiver else { return Ok(PropertySetAction::Rejected(PropertySetRejection::NotObject)); @@ -152,7 +156,7 @@ impl Runtime { SetProbe::Missing(_) => { return self.define_set_receiver(realm, &receiver, key, value, false); } - SetProbe::Special => {} + SetProbe::Special(_) => {} SetProbe::Writable => unreachable!("receiver probe commits a writable data slot"), } let existing = match realm { @@ -284,8 +288,8 @@ impl Runtime { } ReadProbe::Missing(Some(next)) => prototype = Some(next), ReadProbe::Missing(None) => return Ok(NativeConversion::Value(None)), - ReadProbe::Special => { - return self.get_special_or_missing(realm, current, key, receiver); + ReadProbe::Special(kind) => { + return self.get_special_or_missing(kind, realm, current, key, receiver); } } } diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index dad9ae4c..07007b81 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -48,12 +48,30 @@ fn locate( })) } +#[derive(Clone, Copy)] +pub(super) enum SpecialKind { + Proxy, + TypedArray, + ModuleNamespace, + Other, +} + +// Reuse only for the immediate fallback, before any observable operation. +fn special_kind(data: &crate::engine::heap::ObjectData) -> SpecialKind { + match (data.kind, &data.payload) { + (_, ObjectPayload::Proxy(_)) => SpecialKind::Proxy, + (_, ObjectPayload::TypedArray(_)) => SpecialKind::TypedArray, + (ObjectKind::ModuleNamespace, _) => SpecialKind::ModuleNamespace, + _ => SpecialKind::Other, + } +} + pub(super) enum SetProbe { Stored(bool), Writable, Setter(Option), Missing(Option), - Special, + Special(SpecialKind), } impl Runtime { @@ -74,8 +92,9 @@ impl Runtime { let selected = { let mut state = self.0.state.borrow_mut(); let id = object.object_id(); - if !is_ordinary(state.heap.object(id)?) { - return Ok(SetProbe::Special); + let data = state.heap.object(id)?; + if !is_ordinary(data) { + return Ok(SetProbe::Special(special_kind(data))); } match locate(&state, id, key.atom())? { None => { @@ -96,7 +115,7 @@ impl Runtime { } PropertySlot::Accessor { set, .. } => Selected::Setter(*set), PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) => { - return Ok(SetProbe::Special); + return Ok(SetProbe::Special(SpecialKind::Other)); } }, } @@ -125,7 +144,7 @@ pub(super) enum ReadProbe { Value(Value), Getter(Option), Missing(Option), - Special, + Special(SpecialKind), } pub(super) struct OwnFlags { @@ -205,7 +224,7 @@ impl Runtime { let id = object.object_id(); let data = state.heap.object(id)?; if !is_ordinary(data) { - return Ok(ReadProbe::Special); + return Ok(ReadProbe::Special(special_kind(data))); } match locate(&state, id, key.atom())? { None => Selected::Missing(state.heap.shape(data.shape)?.prototype()), @@ -213,7 +232,7 @@ impl Runtime { PropertySlot::Data(value) => Selected::Value(value.clone()), PropertySlot::Accessor { get, .. } => Selected::Getter(*get), PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) => { - return Ok(ReadProbe::Special); + return Ok(ReadProbe::Special(SpecialKind::Other)); } }, } From 9c91663ad61e4acd8b8a6857b4e314069c6a1dc3 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 15:10:18 +0800 Subject: [PATCH 08/14] perf(object): share validation and cleanup boundaries across property layers --- src/engine/object/internal_methods.rs | 16 ++++++++++------ src/engine/object/properties.rs | 11 +++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 037fdbf4..960d62f0 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -934,7 +934,7 @@ impl Runtime { }; return Ok(NativeConversion::Value(Some(value))); } - let own = self.get_own_property(object, key)?; + let own = self.get_own_property_in_operation(object, key)?; if let Some(own) = own { return match own { CompleteOrdinaryPropertyDescriptor::Data { value, .. } => { @@ -1028,7 +1028,6 @@ impl Runtime { value: Value, receiver: Value, ) -> Result, RuntimeError> { - let _operation = self.operation(); match self.prepare_set_property_with_receiver_in_realm( Some(realm), object, @@ -1048,10 +1047,15 @@ impl Runtime { setter, receiver, argument, - } => match self.call_internal(realm, &setter, receiver, &[argument])? { - Completion::Return(_) => Ok(NativeConversion::Value(InternalSetResult::Accepted)), - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), - }, + } => { + let _operation = self.operation(); + match self.call_internal(realm, &setter, receiver, &[argument])? { + Completion::Return(_) => { + Ok(NativeConversion::Value(InternalSetResult::Accepted)) + } + Completion::Throw(value) => Ok(NativeConversion::Throw(value)), + } + } } } diff --git a/src/engine/object/properties.rs b/src/engine/object/properties.rs index 30c90489..b7b848ec 100644 --- a/src/engine/object/properties.rs +++ b/src/engine/object/properties.rs @@ -192,6 +192,17 @@ impl Runtime { ) -> Result, RuntimeError> { let _operation = self.operation(); self.validate_object_and_key(object, key)?; + self.get_own_property_in_operation(object, key) + } + + /// The caller has validated object/key domains and holds an operation + /// guard. Keep the descriptor algorithm shared without nesting public + /// validation/cleanup boundaries on completion-aware Get fallback. + pub(super) fn get_own_property_in_operation( + &self, + object: &ObjectRef, + key: &PropertyKey, + ) -> Result, RuntimeError> { if let Some(snapshot) = self.ordinary_property_snapshot(object, key)? { return match snapshot { Some(snapshot) => self.materialize_property_snapshot(object, key, snapshot), From c37accfc19e6e984d4e0bd0218632e1773eba22e Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 15:37:44 +0800 Subject: [PATCH 09/14] docs(perf): record ordinary property results and reproducible diagnostics --- docs/ordinary-property-plan.md | 38 ++++++++++- scripts/benchmark/ordinary_workloads.py | 90 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 scripts/benchmark/ordinary_workloads.py diff --git a/docs/ordinary-property-plan.md b/docs/ordinary-property-plan.md index 23565799..9e9eaf9d 100644 --- a/docs/ordinary-property-plan.md +++ b/docs/ordinary-property-plan.md @@ -1,6 +1,6 @@ # 普通对象属性访问内核改造计划 -状态:P0–P4 已实现,P5 收口与完整验收进行中。源码核对基线 `fec7519`(2026-09-11)。 +状态:P0–P5 已实现并完成验收。源码核对基线 `fec7519`(2026-09-11)。 面向实施者和评审者:按本文迁移普通属性读、写、定义及元数据查询,并验证语义、所有权和实际性能。 来源为 benchmark issue #16 的对象属性问题;历史热点仅用于选方向,不能作为当前收益预测。 @@ -194,6 +194,40 @@ Test262 fingerprint 变化与实际行为变化分别报告,不能改冻结 re 每个切片记录源码提交、修改拥有者、已运行验证、未运行验证、基线/新 ELF 及工作负载哈希、A/B 分布、 退化和最终处置。原始实验保留在忽略的 target;计划和维护契约入库,不预填结果。 涉及借用/引用事务及 Set 语义收敛的变更需要独立评审,实施者自查不得称为独立评审。 -实现记录:P1 `88a39d1`,P2 `5ad6436`,P3 `68ce00f`,P4 `eaea103`。P5 正在完成特殊分派收口、架构反例和完整验收。 +实现记录:P1 `88a39d1`,P2 `5ad6436`,P3 `68ce00f`,P4 `eaea103`。P5 `bc16d97`,特殊分类/初始借用收口 `41ba53a`,清理与验证边界收口 `9c91663`。 存储实现采用私有 `OwnSlot` 与短借用 probe,不导出 session/slot;白名单同时检查 kind 和 payload。 引用事务由 RuntimeState 统一管理,发布前失败回滚新引用,发布后清理失败不撤销已发布引用。 + + +### 已实现的职责与验收 + +- 自有 Data Set:同一次借用中定位、检查 writable、替换已有槽;不预扫原型、不构造旧值 descriptor、不往返 Define。 +- Get/Get-or-missing:共同定位后只提取 value 或 getter;完整 descriptor 只在其消费者需要时物化。 +- value-only Define:共享槽事务和纯权限判定,保留 SameValue、字段缺失、flags 和跨 Runtime 规则。 +- Has/HasOwn/enumerable:共享定位和 flags;snapshot 与实际 GetOwnProperty 对 AutoInit/VarRef 的不同要求保持。 +- 普通原型链:逐层定位,命中即停;特殊分类在进入回调前复用,异 receiver 单独查找。Array/TypedArray 等特殊对象及普通 AutoInit/VarRef 仍保留相应回退,不把剩余成本称为已经全部消失。 + +最终存储提交以 `9c91663` 为准。独立评审覆盖引用事务、Set 收敛、特殊分类与共享 operation 边界;最后一轮没有阻塞问题。 +Rust 1.88.0 的 workspace 3068 项、实际 QuickJS differential 3072 项、全部 Clippy/feature/doc 组合和 Node/WASM 验收通过;各有原有 ignored 项的两组不计入通过数。 +701 个原有架构反例被拒绝,最终源码扫描和新增 8 个内核反例通过。Test262 focused 6844 项通过,full 102037 项行为向量未变(79982 pass、原有 50 个可运行失败)。 +严格 receipt gate 因源码 fingerprint 变化报告 checksum drift;仅规范化该字段后,TSV/JSONL 完整 SHA-256 与冻结基线相同,未修改冻结 receipts。 + +基线为 #18 `fec7519`(ELF 构建提交 `c1dfce3` 仅增加设计文档);普通 release 使用相同 Rust 1.94.1 和构建 flags、CPU 2、Ryzen 7 7840HS。 +已运行 50+8 固定矩阵和 38 项对象诊断,各五轮交错;超过 3% 的疑似退化追加十轮。各优化提交也与直接前序交错对照。 +最终关键耗时变化:prop_read -11.4%,prop_write -67.0%,prop_update -56.0%,value-only Define -25.6%,enumerable -15.1%。 +自有写入原型深度 0–256 的新耗时约 66–68 ms,基线约 149–1221 ms(包含 setup;另有 setup-only 控制)。八项固定真实程序耗时几何均值 -10.8%。 +剩余退化如实保留:array_read +6.5%,typed_array_read +7.4%,typed_array_write +2.8%(十轮)。实现计划完成不代表所有特殊对象负载都获益。 + +普通 ELF、构建 receipts、工作负载哈希、完整分布、instructions/cycles 和匹配 ELF 的叶采样保留在忽略的 `target/property-delivery-*` / `target/property-final3-*`;完整数据随本次 PR 描述报告。 +首次 pilot、与架构检查重叠的早期 P3 计时,以及 Node 彩色参考输出导致 admission 失败的诊断轮次均不用于结论。差分测试曾遇到并行 feature 构建覆盖 CLI 的测试竞争,已在构建结束后串行重跑通过。 +Rust 1.94.1 的两项 debug native-stack-budget 失败已在未改动 #18 上独立复现;本次完整验收使用仓库固定的 Rust 1.88.0。 + +诊断生成器可复现实际测量的 38 个程序字节、参数及无颜色 Node 参考输出: + +```sh +python3 scripts/benchmark/ordinary_workloads.py --output target/ordinary-diagnostics +python3 scripts/benchmark/fixed.py --manifest target/ordinary-diagnostics/manifest.json \ + --engine before=target/property-baseline/release/qjs \ + --engine after=target/property-final3/release/qjs --repeat 5 --cpu 2 \ + --output target/ordinary-replay +``` diff --git a/scripts/benchmark/ordinary_workloads.py b/scripts/benchmark/ordinary_workloads.py new file mode 100644 index 00000000..2682fc5f --- /dev/null +++ b/scripts/benchmark/ordinary_workloads.py @@ -0,0 +1,90 @@ +"""Generate fixed ordinary-property diagnostics and authenticate outputs with Node. + +Timing belongs to fixed.py. These workloads include process startup, compilation +and setup; depth setup-only controls are separate whole-process measurements. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess + + +def programs(): + for depth in (0, 1, 8, 64, 256): + for operation, count in (("write", 200000), ("setup", 0)): + yield f"own-{operation}-depth-{depth}", depth, ( + f"let p=null; for(let i=0;i<{depth};i++) p=Object.create(p);\n" + "let o=Object.create(p);o.x=0;\n" + f"for(let i=0;i<{count};i++) o.x=i;\nconsole.log(o.x);\n" + ) + + def loop(case, setup, body, result, size=0, count=100000): + return case, size, ( + f"{setup}\nfor(let i=0;i<{count};i++){{{body}}}\n" + f"console.log({result});\n" + ) + + for width in (4, 32, 256, 2048): + for mode in ("shape", "dictionary"): + setup = f'let o={{}}; for(let k=0;k<{width};k++)o["p"+k]=0;' + if mode == "dictionary": + setup += "delete o.p1;" + yield loop(f"write-width-{width}-{mode}", setup, "o.p0=i", "o.p0", width) + values = ( + ("int", "[1,2]", "1"), + ("float", "[1.5,2.5]", "1.5"), + ("string", '["alpha","beta"]', '"alpha"'), + ("symbol", '[Symbol("a"),Symbol("b")]', 'Symbol("a")'), + ("object", "[{},{}]", "{}"), + ) + for kind, alternating, _ in values: + yield loop("write-value-" + kind, "let v=" + alternating + ";let o={x:v[0]};", + "o.x=v[i&1]", "o.x===v[1]") + for args in ( + ("get-data", "let o={x:3};let s=0;", "s+=o.x", "s"), + ("get-inherited", "let o=Object.create({x:3});let s=0;", "s+=o.x", "s"), + ("get-accessor", "let o={get x(){return 3}};let s=0;", "s+=o.x", "s"), + ("set-accessor", "let s=0;let o={set x(v){s=v}};", "o.x=i", "s"), + ("set-receiver", "let p={x:0},o={x:0};", 'Reflect.set(p,"x",i,o)', "o.x"), + ("define-value", "let o={x:0};let d={value:0};", 'd.value=i;Object.defineProperty(o,"x",d)', "o.x"), + ("has-own", "let o={x:1};let s=0;", 'if(Object.hasOwn(o,"x"))s++', "s"), + ("enumerable", "let o={x:1};let s=0;", 'if(o.propertyIsEnumerable("x"))s++', "s"), + ("proxy-set", "let target={x:0};let p=new Proxy(target,{});", "p.x=i", "target.x", 0, 20000), + ("missing-get", "let o=Object.create({});let s=0;", "if(o.x===undefined)s++", "s"), + ): + yield loop(*args) + for kind, _, value in values: + yield "write-same-value-" + kind, 100000, ( + f"let v={value};let o={{x:v}};for(let i=0;i<100000;i++){{o.x=v}} console.log(o.x===v);\n" + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True, help="new workload directory") + parser.add_argument("--oracle", default="node", help="Node executable for output authentication") + args = parser.parse_args() + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=False) + env = {key: value for key, value in os.environ.items() if key != "FORCE_COLOR"} + env["NO_COLOR"] = "1" + rows = [] + for case, size, source in programs(): + path = output / (case + ".js") + path.write_text(source) + result = subprocess.run([args.oracle, str(path)], env=env, capture_output=True, + check=True, timeout=30) + if result.stderr: + raise RuntimeError(f"unexpected oracle stderr for {case}: {result.stderr!r}") + rows.append(dict(case=case, size=size, path=str(path), + sha256=hashlib.sha256(path.read_bytes()).hexdigest(), + expected=result.stdout.decode())) + manifest = output / "manifest.json" + manifest.write_text(json.dumps({"metadata": {"workloads": {"workloads": rows}}}, indent=2) + "\n") + print(f"{len(rows)} authenticated fixed workloads: {manifest}") + + +if __name__ == "__main__": + main() From fd920b6f6d4f11d836a82f6d16c426f75fcebc5f Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 16:42:40 +0800 Subject: [PATCH 10/14] perf(object): inline the validated property read entry point --- src/engine/object/internal_methods.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 960d62f0..85c1eda5 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -896,6 +896,7 @@ impl Runtime { /// than recovering the ordinary-chain missing sentinel. Global binding /// reads depend on that distinction to choose between `undefined` and a /// ReferenceError. + #[inline] pub(crate) fn internal_get_or_missing( &self, realm: ContextId, From 416aa1a23db81bee59365cfa83a36ea2463794f7 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 16:46:57 +0800 Subject: [PATCH 11/14] perf(object): reuse dense values and typed buffer access snapshots --- .../builtins/array_buffer/typed_array.rs | 23 ++++++++--- src/engine/object/internal_methods.rs | 1 - src/engine/object/ordinary_storage.rs | 31 +++++++++------ src/engine/object/ordinary_tests.rs | 38 +++++++++++++++++++ 4 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/engine/builtins/array_buffer/typed_array.rs b/src/engine/builtins/array_buffer/typed_array.rs index 42482460..8c7dd051 100644 --- a/src/engine/builtins/array_buffer/typed_array.rs +++ b/src/engine/builtins/array_buffer/typed_array.rs @@ -1614,6 +1614,15 @@ impl Runtime { snapshot: TypedArraySnapshot, ) -> Result { let buffer = self.snapshot_buffer_access(snapshot.buffer)?.state; + Ok(Self::typed_array_state_with_buffer(snapshot, buffer)) + } + + // Pure bounds calculation, shared with element access. A caller holding + // an access token must not perform observable work before consuming it. + fn typed_array_state_with_buffer( + snapshot: TypedArraySnapshot, + buffer: crate::engine::heap::ArrayBufferState, + ) -> TypedArrayState { let width = u32::from(snapshot.element.byte_length()); let byte_length = if buffer.detached || snapshot.byte_offset > buffer.byte_length { None @@ -1637,13 +1646,13 @@ impl Runtime { // RAB grows to a byte length not divisible by the element width. let length = byte_length / width; let byte_length = length * width; - Ok(TypedArrayState { + TypedArrayState { snapshot, length, byte_length, out_of_bounds, resizable: buffer.max_byte_length.is_some(), - }) + } } pub(crate) fn typed_array_current_length( @@ -1786,13 +1795,14 @@ impl Runtime { object: &ObjectRef, index: u64, ) -> Result, RuntimeError> { - let state = self.typed_array_state(object)?; + let snapshot = self.typed_array_snapshot(object)?; + let access = self.snapshot_buffer_access(snapshot.buffer)?; + let state = Self::typed_array_state_with_buffer(snapshot, access.state); if state.out_of_bounds || index >= u64::from(state.length) { return Ok(None); } let absolute = typed_array_absolute_byte_offset(state.snapshot, index)?; let width = usize::from(state.snapshot.element.byte_length()); - let access = self.snapshot_buffer_access(state.snapshot.buffer)?; let bytes = self.read_buffer_word(&access, absolute, width)?; Ok(Some(typed_array_decode(state.snapshot.element, bytes))) } @@ -1898,13 +1908,14 @@ impl Runtime { index: u64, bytes: &[u8; 8], ) -> Result { - let state = self.typed_array_state(object)?; + let snapshot = self.typed_array_snapshot(object)?; + let access = self.snapshot_buffer_access(snapshot.buffer)?; + let state = Self::typed_array_state_with_buffer(snapshot, access.state); if state.out_of_bounds || index >= u64::from(state.length) { return Ok(false); } let absolute = typed_array_absolute_byte_offset(state.snapshot, index)?; let width = usize::from(state.snapshot.element.byte_length()); - let access = self.snapshot_buffer_access(state.snapshot.buffer)?; self.write_buffer_word(&access, absolute, &bytes[..width])?; Ok(true) } diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 85c1eda5..960d62f0 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -896,7 +896,6 @@ impl Runtime { /// than recovering the ordinary-chain missing sentinel. Global binding /// reads depend on that distinction to choose between `undefined` and a /// ReferenceError. - #[inline] pub(crate) fn internal_get_or_missing( &self, realm: ContextId, diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index 07007b81..41b2e6bb 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -223,18 +223,27 @@ impl Runtime { let state = self.0.state.borrow(); let id = object.object_id(); let data = state.heap.object(id)?; - if !is_ordinary(data) { + // Dense elements are own data properties. Read the value under + // this same classification borrow; holes and named properties + // still use the exotic descriptor/prototype algorithm. + if let ObjectPayload::Array { dense: Some(dense) } = &data.payload + && let Some(index) = state.atoms.array_index(key.atom())? + && let Some(value) = dense.get(index as usize) + { + Selected::Value(value.clone()) + } else if !is_ordinary(data) { return Ok(ReadProbe::Special(special_kind(data))); - } - match locate(&state, id, key.atom())? { - None => Selected::Missing(state.heap.shape(data.shape)?.prototype()), - Some(slot) => match &data.slots[slot.index] { - PropertySlot::Data(value) => Selected::Value(value.clone()), - PropertySlot::Accessor { get, .. } => Selected::Getter(*get), - PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) => { - return Ok(ReadProbe::Special(SpecialKind::Other)); - } - }, + } else { + match locate(&state, id, key.atom())? { + None => Selected::Missing(state.heap.shape(data.shape)?.prototype()), + Some(slot) => match &data.slots[slot.index] { + PropertySlot::Data(value) => Selected::Value(value.clone()), + PropertySlot::Accessor { get, .. } => Selected::Getter(*get), + PropertySlot::AutoInit(_) | PropertySlot::VarRef(_) => { + return Ok(ReadProbe::Special(SpecialKind::Other)); + } + }, + } } }; Ok(match selected { diff --git a/src/engine/object/ordinary_tests.rs b/src/engine/object/ordinary_tests.rs index 052a9aa3..365c868e 100644 --- a/src/engine/object/ordinary_tests.rs +++ b/src/engine/object/ordinary_tests.rs @@ -209,3 +209,41 @@ fn ordinary_property_replacing_last_heap_edge_reclaims_old_object() { context.set_property(&object, &key, Value::Null).unwrap(); assert!(runtime.0.state.borrow().heap.object(old_id).is_err()); } + +#[test] +fn ordinary_property_dense_reads_preserve_holes_receivers_and_transitions() { + check( + r#" + var token={}; var a=[token, 2, 3]; + var own=a[0]===token && Reflect.get(a,'0',{})===token; + delete a[1]; + var proto=Object.create(Array.prototype); + Object.defineProperty(proto,'1',{get(){return this.marker},configurable:true}); + Object.setPrototypeOf(a,proto); a.marker=7; + var hole=a[1]===7 && Reflect.get(a,'1',{marker:9})===9; + Object.defineProperty(a,'2',{get(){return this.marker+1},configurable:true}); + var accessor=a[2]===8; + Object.defineProperty(a,'0',{value:token,writable:false}); + own && hole && accessor && a[0]===token && !Reflect.set(a,'0',4); + "#, + ); +} + +#[test] +fn ordinary_property_typed_access_revalidates_after_conversion() { + check( + r#" + var b=new ArrayBuffer(4,{maxByteLength:8}); + var tracking=new Uint8Array(b); var fixed=new Uint8Array(b,0,4); + tracking[3]={valueOf(){b.resize(2);return 9}}; + var shrunk=tracking[3]===undefined && fixed[0]===undefined; + tracking[3]={valueOf(){b.resize(8);return 11}}; + var grown=tracking[3]===11 && fixed[3]===11; + fixed[0]={valueOf(){b.transfer();return 42}}; + var detached=tracking[0]===undefined && fixed[0]===undefined; + var shared=new Uint16Array(new SharedArrayBuffer(4)); + shared[1]=513; + shrunk && grown && detached && shared[1]===513; + "#, + ); +} From 580da0f1eb3c8bf392529c5f3f55d9c9dde01509 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 16:58:57 +0800 Subject: [PATCH 12/14] perf(object): skip dense index parsing for named properties --- src/engine/object/ordinary_storage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index 41b2e6bb..d989e609 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -226,8 +226,8 @@ impl Runtime { // Dense elements are own data properties. Read the value under // this same classification borrow; holes and named properties // still use the exotic descriptor/prototype algorithm. - if let ObjectPayload::Array { dense: Some(dense) } = &data.payload - && let Some(index) = state.atoms.array_index(key.atom())? + if let Some(index) = key.atom().immediate_integer() + && let ObjectPayload::Array { dense: Some(dense) } = &data.payload && let Some(value) = dense.get(index as usize) { Selected::Value(value.clone()) From 1cc51bb5fcc5c36912d3197d877219ae513dc4b5 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 17:05:05 +0800 Subject: [PATCH 13/14] perf(object): share own-slot reads for genuine arrays --- src/engine/object/ordinary_storage.rs | 13 ++++++++++--- src/engine/object/ordinary_tests.rs | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index d989e609..ec76202e 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -223,18 +223,25 @@ impl Runtime { let state = self.0.state.borrow(); let id = object.object_id(); let data = state.heap.object(id)?; + let is_array = matches!( + (data.kind, &data.payload), + (ObjectKind::Array, ObjectPayload::Array { .. }) + ); // Dense elements are own data properties. Read the value under - // this same classification borrow; holes and named properties - // still use the exotic descriptor/prototype algorithm. + // this same classification borrow. Other own Array slots share + // value/getter selection; exotic misses retain their fallback. if let Some(index) = key.atom().immediate_integer() && let ObjectPayload::Array { dense: Some(dense) } = &data.payload && let Some(value) = dense.get(index as usize) { Selected::Value(value.clone()) - } else if !is_ordinary(data) { + } else if !is_ordinary(data) && !is_array { return Ok(ReadProbe::Special(special_kind(data))); } else { match locate(&state, id, key.atom())? { + // A miss may still be a non-immediate dense index. Leave + // exotic misses to the complete Array lookup algorithm. + None if is_array => return Ok(ReadProbe::Special(SpecialKind::Other)), None => Selected::Missing(state.heap.shape(data.shape)?.prototype()), Some(slot) => match &data.slots[slot.index] { PropertySlot::Data(value) => Selected::Value(value.clone()), diff --git a/src/engine/object/ordinary_tests.rs b/src/engine/object/ordinary_tests.rs index 365c868e..d68954fc 100644 --- a/src/engine/object/ordinary_tests.rs +++ b/src/engine/object/ordinary_tests.rs @@ -215,7 +215,11 @@ fn ordinary_property_dense_reads_preserve_holes_receivers_and_transitions() { check( r#" var token={}; var a=[token, 2, 3]; - var own=a[0]===token && Reflect.get(a,'0',{})===token; + var denseLength=a.length===3 && a[0]===token && Reflect.get(a,'0',{})===token; + var symbol=Symbol(); a[symbol]=token; a['01']=5; a[2147483648]=6; + var large=a[2147483648]===6; delete a[2147483648]; a.length=3; + var own=a[0]===token && Reflect.get(a,'0',{})===token + && denseLength && a.length===3 && a['01']===5 && a[symbol]===token && large; delete a[1]; var proto=Object.create(Array.prototype); Object.defineProperty(proto,'1',{get(){return this.marker},configurable:true}); From c52d4dc7747641756dff8cb7159b9885e9cc8b17 Mon Sep 17 00:00:00 2001 From: Eric-Song-Nop Date: Fri, 11 Sep 2026 17:25:05 +0800 Subject: [PATCH 14/14] docs: record array and typed-array regression fixes and final measurements --- docs/ordinary-property-plan.md | 37 ++++++++++++++++++++++++++++------ src/engine/object/README.md | 2 ++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/ordinary-property-plan.md b/docs/ordinary-property-plan.md index 9e9eaf9d..19031f2c 100644 --- a/docs/ordinary-property-plan.md +++ b/docs/ordinary-property-plan.md @@ -199,7 +199,7 @@ Test262 fingerprint 变化与实际行为变化分别报告,不能改冻结 re 引用事务由 RuntimeState 统一管理,发布前失败回滚新引用,发布后清理失败不撤销已发布引用。 -### 已实现的职责与验收 +### P5 首次验收(9c91663) - 自有 Data Set:同一次借用中定位、检查 writable、替换已有槽;不预扫原型、不构造旧值 descriptor、不往返 Define。 - Get/Get-or-missing:共同定位后只提取 value 或 getter;完整 descriptor 只在其消费者需要时物化。 @@ -207,18 +207,18 @@ Test262 fingerprint 变化与实际行为变化分别报告,不能改冻结 re - Has/HasOwn/enumerable:共享定位和 flags;snapshot 与实际 GetOwnProperty 对 AutoInit/VarRef 的不同要求保持。 - 普通原型链:逐层定位,命中即停;特殊分类在进入回调前复用,异 receiver 单独查找。Array/TypedArray 等特殊对象及普通 AutoInit/VarRef 仍保留相应回退,不把剩余成本称为已经全部消失。 -最终存储提交以 `9c91663` 为准。独立评审覆盖引用事务、Set 收敛、特殊分类与共享 operation 边界;最后一轮没有阻塞问题。 +P5 首次验收的存储提交为 `9c91663`。独立评审覆盖引用事务、Set 收敛、特殊分类与共享 operation 边界;最后一轮没有阻塞问题。 Rust 1.88.0 的 workspace 3068 项、实际 QuickJS differential 3072 项、全部 Clippy/feature/doc 组合和 Node/WASM 验收通过;各有原有 ignored 项的两组不计入通过数。 701 个原有架构反例被拒绝,最终源码扫描和新增 8 个内核反例通过。Test262 focused 6844 项通过,full 102037 项行为向量未变(79982 pass、原有 50 个可运行失败)。 严格 receipt gate 因源码 fingerprint 变化报告 checksum drift;仅规范化该字段后,TSV/JSONL 完整 SHA-256 与冻结基线相同,未修改冻结 receipts。 基线为 #18 `fec7519`(ELF 构建提交 `c1dfce3` 仅增加设计文档);普通 release 使用相同 Rust 1.94.1 和构建 flags、CPU 2、Ryzen 7 7840HS。 已运行 50+8 固定矩阵和 38 项对象诊断,各五轮交错;超过 3% 的疑似退化追加十轮。各优化提交也与直接前序交错对照。 -最终关键耗时变化:prop_read -11.4%,prop_write -67.0%,prop_update -56.0%,value-only Define -25.6%,enumerable -15.1%。 +首次验收关键耗时变化:prop_read -11.4%,prop_write -67.0%,prop_update -56.0%,value-only Define -25.6%,enumerable -15.1%。 自有写入原型深度 0–256 的新耗时约 66–68 ms,基线约 149–1221 ms(包含 setup;另有 setup-only 控制)。八项固定真实程序耗时几何均值 -10.8%。 -剩余退化如实保留:array_read +6.5%,typed_array_read +7.4%,typed_array_write +2.8%(十轮)。实现计划完成不代表所有特殊对象负载都获益。 +首次验收发现的退化:array_read +6.5%,typed_array_read +7.4%,typed_array_write +2.8%(十轮)。这些退化已在后续修复中消除,见下节。 -普通 ELF、构建 receipts、工作负载哈希、完整分布、instructions/cycles 和匹配 ELF 的叶采样保留在忽略的 `target/property-delivery-*` / `target/property-final3-*`;完整数据随本次 PR 描述报告。 +普通 ELF、构建 receipts、工作负载哈希、完整分布、instructions/cycles 和匹配 ELF 的叶采样保留在忽略的 `target/property-delivery-*` / `target/property-final3-*`;当轮本地数据保留用于前后对照。 首次 pilot、与架构检查重叠的早期 P3 计时,以及 Node 彩色参考输出导致 admission 失败的诊断轮次均不用于结论。差分测试曾遇到并行 feature 构建覆盖 CLI 的测试竞争,已在构建结束后串行重跑通过。 Rust 1.94.1 的两项 debug native-stack-budget 失败已在未改动 #18 上独立复现;本次完整验收使用仓库固定的 Rust 1.88.0。 @@ -228,6 +228,31 @@ Rust 1.94.1 的两项 debug native-stack-budget 失败已在未改动 #18 上独 python3 scripts/benchmark/ordinary_workloads.py --output target/ordinary-diagnostics python3 scripts/benchmark/fixed.py --manifest target/ordinary-diagnostics/manifest.json \ --engine before=target/property-baseline/release/qjs \ - --engine after=target/property-final3/release/qjs --repeat 5 --cpu 2 \ + --engine after=target/property-regfix3/release/qjs --repeat 5 --cpu 2 \ --output target/ordinary-replay ``` + + +### Array / TypedArray 退化修复(1cc51bb) + +Array 的直接编码稠密索引和真正 Array 的已有自有槽共享 value/getter 选择,不构造完整 descriptor;Array 未命中、需要回退的非直接编码稠密索引、AutoInit/VarRef 仍走原算法。读取扩展不放宽普通 Set/Define 白名单。命名属性不预先解析数组索引,避免 `length` 读取额外开销。 +TypedArray 的边界计算和字节访问复用同一次访问中的 owned buffer token,边界计算抽成纯函数供原有状态查询共享。写入仍在可能执行用户代码的值转换之后重新获取 token,不跨回调缓存状态;所有元素类型共用该机制。 + +最终 engine 为 `1cc51bb`,仍与 #18 的普通 release 在相同 Rust 1.94.1、flags 和 CPU 2 上比较。重点七项同时与旧 #19 `9c91663` 做十轮轮换顺序三方对照;完整 58 项矩阵及 38 项对象诊断另各做五轮交错,所有样本输出校验通过,计时没有与编译/测试重叠。 + +| 操作 | 相对 #18 耗时变化(十轮中位数) | +| --- | ---: | +| 数组元素读取 | -18.2% | +| 数组 length 读取 | -28.8% | +| TypedArray 元素读取 | -12.9% | +| TypedArray 元素写入 | -18.9% | +| 普通属性读取 | -13.5% | +| 普通属性写入 | -65.4% | +| 普通属性更新 | -54.7% | + +完整 50 项微基准耗时几何均值 -8.6%,8 项真实程序 -12.7% 且全部改善。最终 58+38 项没有超过 3% 的退化;不把这一阈值表述为每个负载都更快。五轮 instructions/cycles 对照也确认四项 Array/TypedArray 重点负载的指令数和 cycles 均减少。 + +最终源码的 Rust 1.88.0 workspace/all-targets + test262-host + pinned QuickJS 对照为 3074 passed、0 failed、1 原有 ignored;包含 14 项属性回归,覆盖稠密/慢 Array、空洞和原型 getter、receiver、命名/Symbol/大索引,以及转换期间 resize/detach 与共享 buffer。13 组 QuickJS fixture、19 组 C oracle、三种 Clippy 配置、15 个 Web/WASM 示例、格式/布局、架构扫描和 16 项架构测试通过。 +全量 Test262 102037 个结果逐项未变(79982 pass、80032 runnable、同样 50 个 runnable failures)。仅规范化源码指纹 `e4f461c60faf9116313fcbcc6fc062d691f308ad4042e7f98d9a9154d21deea3` 后,完整 TSV/JSONL SHA-256 与冻结基线相同;严格 receipt gate 的源码指纹差异和语义结果差异分别处理,没有修改冻结 receipts。 + +最终普通 ELF、构建 receipts、完整样本/分布、stdout/stderr、计数器与验证记录保留在忽略的 `target/property-regfix3-*`;完整性能表见 [PR #19](https://github.com/pocket-stack/quickjs-oxide/pull/19)。仅内联的尝试已撤回,中间候选的命名属性退化在最终版本中修复;中间版本不混入最终性能表。 diff --git a/src/engine/object/README.md b/src/engine/object/README.md index bba50c34..1cec56af 100644 --- a/src/engine/object/README.md +++ b/src/engine/object/README.md @@ -7,6 +7,8 @@ 普通属性读写与定义的职责和验收见[普通对象属性访问内核改造计划](../../../docs/ordinary-property-plan.md)。 `ordinary_storage` 在一次借用内完成分类、定位与槽更新;`ordinary` 拥有普通对象算法, `internal_methods` 拥有特殊对象和 completion 分发。槽索引不跨回调逃逸;只 root 当前操作需要的值。 +真正 Array 的自有槽和直接编码稠密索引共享按需读取;未命中仍进入特殊对象回退, +Set/Define 的普通对象白名单不因此扩大。TypedArray 字节访问与边界检查的 token 复用由 builtins 负责,不能跨值转换回调。 Dictionary 模式用于普通对象和已转为慢表示的 Array。共享 shape 首次分离,独占 shape 原地转换并脱离 weak cache。物理槽用 swap-remove,插入顺序由独立双向链接维护;