diff --git a/Cargo.toml b/Cargo.toml index def0f75c..43b6d72d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,10 @@ unsafe_code = "forbid" [workspace.lints.clippy] all = { level = "warn", priority = -1 } +[profile.release] +lto = "fat" +codegen-units = 1 + [profile.web] inherits = "release" opt-level = "s" diff --git a/docs/reports/performance-architecture.md b/docs/reports/performance-architecture.md new file mode 100644 index 00000000..9165b495 --- /dev/null +++ b/docs/reports/performance-architecture.md @@ -0,0 +1,593 @@ +# 性能架构:8B 值表示 + quickening + 数据导向堆(无 JIT、默认无 unsafe) + +> 状态:设计定稿,待评审与分阶段实施。本文档取代 +> `performance-plan.md`「横向设计比较」之后悬置的 S3 定义;S0–S2 是常数因子 +> 路线,S3 起升级为结构路线。目标不是「补齐常数」,而是在无 JIT 约束下对齐 +> 并在特定轴上超过 QuickJS。 + +--- + +## 0. 输入与硬约束 + +输入文档:`performance-plan.md`(S0–S2 证据与横向比较)、`parity.md`、 +`status.md`、`profiling.md`。外部证据见附录 A。 + +硬约束: + +1. **无 JIT**(`performance-plan.md:670`)。上限是「极致解释器」。copy-and-patch、 + 动态代码复制、wasm-as-format 均属 JIT 近亲,不采纳。 +2. **GC 模型被契约钉死**:`parity.md:183-191`(§13)要求保持 QuickJS 式确定性 + RC + 循环回收,finalize 时机可观察。因此「换 tracing GC 消灭 RC 流量」 + **不在 S3 范围**;S3 内 RC 只能做便宜,不能消灭。契约修订情形下的 + tracing 迁移列为 **S4 候选(推迟,非否决)**,决策门禁见 §10。 +3. **unsafe 政策**:`parity.md:23` 允许受审计 `unsafe`,但 workspace 现状是 + `unsafe_code = "forbid"`(`Cargo.toml:46-47`),且 `status.md:3` 对外宣称 + "unsafe-free"。S3 的默认路线**零 unsafe**;受审计 unsafe 只作保留席位 + (方案 F),引入是显性治理动作,不得在性能 PR 里悄悄发生。 +4. **一致性门禁**:Test262 冻结向量(`pass=79982 / eligible=80032 / + total=102037`)零回归;不得为性能改动修订一致性基线 + (`scripts/benchmark/README.md:175-176`)。 +5. **测量纪律**:正式计时用非 profiling 的普通 release 构建、串行、独立输出 + 目录、留 receipts(`profiling.md:288`;benchmark README)。S3 第一阶段 + 必须先重定基线(方案 E)。 +6. 单线程执行核心:`Rc`/`RefCell`/`Cell` 语义不变;不引入 `Send`/`Sync` + 共享(`parity.md:243` worker 模型不变)。 + +## 1. 关键架构事实(现状证据) + +以下事实是 S3 选型的依据(全部已核实,含文件行号): + +1. **32B `Value` 的根因是「每个值背着 `Rc`」,不是 enum 本身。** + `ObjectRef = Runtime(Rc, 8B) + ObjectId(8B)`(`src/engine/object/mod.rs:28-31`); + `SymbolRef = AtomOwner(Rc) + Atom(16B) = 24B`(`object/mod.rs:374,135-138`), + 它把 `Value`(`src/engine/value/mod.rs:13-24`)撑到 32B。该 Rc 的唯一作用是 + 让值在没有 runtime 参数时也能 retain/release/drop 并做跨 runtime 防护—— + QuickJS 用显式 `JS_FreeValue(ctx, v)` 解决同一问题。**把 Rc 逐出内部值是 + 8B 的入场券,这是所有权纪律问题,不是 unsafe 问题。** +2. **句柄天然是索引**:`ObjectId { index: u32, generation: u32 }` + (`src/engine/heap/identity.rs:7-11`),解引用本来就是 arena 下标访问—— + 安全的 bounds-checked load。把 u32 索引装进 NaN payload 只需纯整数位运算, + **索引 NaN-box 不需要 unsafe**。最坏情况是 bug 变成 trusted-path panic 或 + leak,不是 UB。 +3. **RC 单价已降、次数仍在**:S1 已把热路径 retain 改成 `&self` + `Cell` + 饱和加(`retain_raw_fast`,`src/engine/heap/gc.rs:1059`),但值每次复制/ + 销毁仍各有一次计数操作,且 Object 克隆仍走 `try_borrow_mut` + 校验 + (`copy_reference`,`src/engine/vm/stack.rs:1419`)。RC 次数是语义性的, + 只能降单价(方案 A/D),不能归零(约束 2)。 +4. **借检查是全局单点**:`RefCell` 全仓库 981 个借用站点 + (`runtime/mod.rs:31-62`;`vm/driver.rs` 122 处为最密)。热路径已尽量 + `Cell`/`try_borrow`,但「单线程可变性证明」在运行期付费。 +5. **字节码不可变 + 侧表可变是既有先例**:`code: Rc<[Instruction]>` + (`code/executable.rs:206`)发布即冻结,`FusionPlan` 用 per-PC 标志字节 + 侧表做静态超指令(`code/fusion.rs:1-6`),IC 用 `Cell` 挂在共享 + snapshot 上(`object/property_ic.rs:23-37`)。「不可变规范 + 可变执行态」 + 的信任模型已经存在,quickening(方案 B)是它的自然延伸。 +6. **存量浪费清单**(与安全无关,纯自残税): + - `ArenaSlot = 440B`:全 kinds 混排单一 `Vec` + (`src/engine/heap/mod.rs:232-237`;测试断言 `src/engine/heap/edges.rs:124`), + 由最大的 `ObjectData` 撑起; + - 对象无内联槽:`ObjectData.slots: Vec` + (`src/engine/heap/object_records.rs:461-484`),每个对象至少一次独立分配; + - 属性键 16B:`Atom { raw, generation, table_id }`(`src/engine/atom/mod.rs:52-57`), + 相等比较逐 16B(shape ≤8 项线性扫描全命中); + - atom 字符串表用 SipHash(`atom/mod.rs:280-294`),且 `JsString` 不缓存 + hash,每次 probe 全串重算(`src/engine/value/primitive.rs:1249-1256`); + - 任一 prototype 被写即 bump 堆全局 `property_layout_epoch` + (`src/engine/heap/object_storage.rs:10-18`),**全堆 depth>0 的 IC 条目 + 集体失效**——V8 用 per-prototype validity cell,粒度天差地别; + - shape 迁移存 runtime 全局嵌套 HashMap、键是 24B `ShapeEntry` + (`runtime/mod.rs:112-113`); + - 派发循环每步付 bounds-checked fetch + `checked_add` + `Result` 管道 + (`src/engine/vm/run.rs:316-328`),指令是 ~16B/条的 198-variant enum + (`code/bytecode.rs:123`)。 +7. **`[profile.release]` 从未调过**:`Cargo.toml:51-57` 只有 web profile; + native release 是 cargo 默认(无 LTO、16 CGU)。PGO/LTO 是未领取的 + 免费收益。 + +## 2. 方案总览 + +按依赖与收益排序(E 先行,A 是地基,B 是差异化武器,D/C 与 A 复利): + +| 方案 | 内容 | unsafe | 预期量级 | 依据 | +| --- | --- | --- | --- | --- | +| **E** | 构建基线:LTO + CGU=1 + PGO(BOLT 可选) | 无 | 8–20% | 附录 A.7 | +| **A** | 8B 值表示:索引 NaN-box,Rc 逐出内部值 | 无 | ~1.5–2×(值流量密集路径) | §1.1–1.3 | +| **B** | quickening + 可变执行 IR(QuickJS 没有) | 无 | +10–25% | 附录 A.1–A.3 | +| **D** | 数据导向堆:typed arena、内联槽、validity cell、atom/string 便宜化 | 无 | +10–30%(对象/数组密集) | §1.6 | +| **C** | 派发与栈流量:TOS/accumulator 缓存、扩展静态超指令、可选 fn-pointer threading | 无 | +5–15% | 附录 A.4–A.6 | +| **F** | 受审计 unsafe 保留席位:仅在测量点名后逐点引入 | 受审计 | 视点名位置 | §8 | + +不采纳:寄存器式 VM 全面重写、nightly `become`、copy-and-patch / +动态复制(理由见 §9)。**S4 候选:RC → tracing GC——推迟而非否决**, +双门禁见 §10。 + +## 3. E:构建基线(第一阶段,先于一切测量) + +- `Cargo.toml` 增加 `[profile.release] lto = "fat", codegen-units = 1`; + 建立 PGO 流程(`-Cprofile-generate` / `-Cprofile-use`),训练负载用 + `scripts/benchmark/scaling.py` + v8-v7 套件。BOLT 作为可选后续。 +- 用同一 flags 重建 pre-S3 基线二进制并留 receipts;S3 之后所有对比都以 + **PGO 后的基线**为分母(否则把编译器布局噪声当设计收益)。 +- 提交粒度:profile 改动与 PGO 流程脚本各一个 commit;无语义变更,门禁走 + `cargo test --locked --workspace --all-targets` + test262 `--check`(源码 + 哈希会变,需重跑 `--full` 出 current-source receipt,不改 `current.conf`)。 +- 生效范围:`lto = "fat"` + `codegen-units = 1` 对**任何 `cargo build --release` + 自动生效**(含 CLI、`build.py`);**PGO 不会**——它需要 `pgo.py` 的两阶段 + `RUSTFLAGS=-Cprofile-use`,默认 release 构建没有 profile,需显式跑 PGO 流程 + 才能得到完整方案 E 的二进制。 + +### E 实测(相对 pre-S3 HEAD `06386457`) + +在独立 worktree、同机串行构建并测量三个普通 release 二进制,无并发构建/测试: + +| 标签 | 配置 | +| --- | --- | +| `baseline` | pre-S3 HEAD 默认 release(无 LTO、16 CGU) | +| `lto` | `lto = "fat"` + `codegen-units = 1` | +| `pgo` | LTO/CGU=1 + PGO;训练负载 = `scaling.py` 全 22 case×{64,128} + v8-v7 全 8 suite | + +`pgo.py` 逐进程设置唯一 `LLVM_PROFILE_FILE=/%m_%p.profraw`,再 +`llvm-profdata merge` 合并(否则每进程覆盖同一个 `default_%m_%c.profraw`,只剩 +最后一次训练负载);产物带 `.build.json` receipt,记录 profdata 哈希与训练负载。 + +**属性读探针(`property_read_probe.py`,N=5,000,000,repeat 7,median ns/op):** + +| case | baseline | lto | pgo | lto 变化 | pgo 变化 | +| --- | ---: | ---: | ---: | ---: | ---: | +| prop_read_int | 176.23 | 160.64 | 112.34 | −8.8% | −36.3% | +| prop_read_obj | 215.68 | 203.83 | 135.83 | −5.5% | −37.0% | +| prop_read_string | 271.43 | 249.57 | 198.37 | −8.1% | −26.9% | + +**`scaling.py`(22 case × 2 size = 44 cell,operations=32768,repeat 3,整进程 wall):** +`lto` geomean `0.898×`;`pgo` geomean `0.696×`、中位 `0.685×`(**−31.5%**), +区间 0.555–0.996(最好 cell `array-holey`、`array-index`、`set`,最差 `scope`)。 + +**V8-v7(Score,越高越好,repeat 3):** + +| case | baseline | lto | pgo | lto 变化 | pgo 变化 | +| --- | ---: | ---: | ---: | ---: | ---: | +| richards | 50.7 | 55.7 | 87.6 | +9.9% | +72.8% | +| deltablue | 64.1 | 72.2 | 110.0 | +12.6% | +71.6% | +| crypto | 62.9 | 65.6 | 101.0 | +4.3% | +60.6% | +| raytrace | 95.7 | 105.0 | 143.0 | +9.7% | +49.4% | +| earley-boyer | 117 | 127 | 181 | +8.5% | +54.7% | +| regexp | 88.5 | 90.5 | 138 | +2.3% | +55.9% | +| splay | 320 | 349 | 483 | +9.1% | +50.9% | +| navier-stokes | 255 | 299 | 422 | +17.3% | +65.5% | +| **geomean** | | | | **+9.1%** | **+60.0%** | + +**`microbench`(ms 分辨率,仅定性,min ns/op):** `empty_loop` 100→50→40、 +`prop_read` 250→125→100、`array_read` 200→200→100、`func_call` 500→500→250; +`int_arith` 三档均为 200、未分辨。 + +**结论:** 方案 E 的实测收益超出 §2 预计的 8–20%——LTO+CGU=1 约 5–9%, +PGO 把属性读延迟压低 27–37%、V8-v7 Score 整体抬高 **1.60×**、混合整进程负载 +geomean **0.696×**(≈1.44× 吞吐)。这是纯构建层收益、无语义改动,成功为 +后续 A/B/D 建立更高的比较基线。 + +**门禁:** `cargo fmt --check`、`check-source-layout.py`、workspace +`cargo test --locked --workspace --all-targets`、benchmark 单测(22)全部通过; +Test262 `--check` 如预期报 baseline 源码过期(`Cargo.toml` 在 +`engine_semantics_files` 内),`--full` 重跑得到 current-source receipt: +`complete Test262 vector matches: 79982 pass of 80032 eligible (102037 total)`, +零回归,`current.conf` 未改。 + +## 4. A:8B 值表示——索引 NaN-box(零 unsafe) + +> 实施拆分(A0–A4)与三个设计点(内部 `JsValue`/API 边界、String/BigInt +> 堆化、`Atom` 瘦身)已钉死于 **`docs/reports/s3-a-plan.md`**;本节为设计 +> 概要,冲突处以 s3-a-plan.md 为准。 + +### 4.1 编码 + +```rust +pub struct JsValue(u64); // 内部执行值;不实现 Copy/Drop +``` + +- `Float(f64)`:按位原样存储(与 QuickJS 同等待遇,浮点不装箱); +- `Int(i32)` / `Bool` / `Null` / `Undefined`:tag 空间内联; +- Object / String / Symbol / BigInt:NaN payload 52-bit 内装 `kind | u32 index`, + 解引用 = typed arena 的安全下标访问(保留 bounds check;generation 校验维持 + 现状的「可信路径 debug-only、边界全量」分层); +- 全部纯整数位运算,**无 union、无 transmute、无裸指针**。 + +### 4.2 所有权纪律(核心改动) + +- `JsValue` 不实现 `Copy`/`Drop`:VM 在覆盖槽、弹栈、拆帧时显式 release + (此刻调用点本就有 `&RuntimeState`/`&Heap`);复制即显式 dup(trusted + `Cell` 递增,S1 已就位)。这是 QuickJS 的 C 纪律搬进安全 Rust:Rust 不 + 强制,出错形态是 leak 或 trusted-path panic,不是 UB。 +- 公共 API 保留现有带 Rc 的 root 类型(`ObjectRef` 等),只在 API 边界做 + root/unroot 转换;root 类型继续提供 Drop 语义与跨 runtime 防护。 +- `Atom` 内部瘦身为 `u32` newtype;16B branded 形式只留不可信边界。论证与 + `live_node_fast` 相同:活 shape/字节码/IC 持有的 atom 必然被其 owner retain, + 可信路径免品牌校验。atom refcount 顺带 `Cell` 化(收尾 S1b,`Symbol` 不再 + 从快路 decline)。 + +### 4.3 退路 + +若位编码需分阶段:先落 `enum { Int(i32), Float(f64), Object(u32), … }` +(16B,f64 内联、u32 句柄),零编码风险,已比现状小一半;NaN-box 作为 +第二步。注意 Nova 式 8B enum(句柄 + boxed f64)**不采纳**:浮点上堆会在 +算术密集路径引入分配,比 NaN-box 差。 + +### 4.4 级联收益 + +- `RawValue`(`identity.rs:200-224`)24B→8B;`PropertySlot::Data` 同减, + 属性内存减半; +- `FrameBinding`(`vm/bindings.rs:17-23`)40B→~12B,操作数栈槽同减; +- `copy_value`(`vm/stack.rs:1402`,S0 实测 ~8%)标量臂从 73B outlined + 变为一条 `mov`;值搬运总量降 4×; +- 每条 64B 缓存行放 8 个值(现状 2 个)。 + +### 4.5 分步提交 + +下列草单已被 `s3-a-plan.md` 的 A0–A4 拆分取代(新增 A0 地基与 A1 +String/BigInt 堆化两个阶段,原子瘦身提前至 A0-a),保留仅供追溯: + +1. `refactor(value): introduce handle-based internal value type` —— 新类型 + + 转换层,先不接线; +2. `refactor(atom): slim internal atom to u32 index` —— 边界保留 branded; +3. `refactor(vm): operate on internal values in slots and frames` —— FrameBinding / + SlotStore / run.rs 切换,显式 dup/release; +4. `refactor(heap): store 8-byte raw values` —— RawValue / PropertySlot 切换; +5. `perf(value): nan-box encoding` ——(或先停在 16B enum 退路); +6. `docs(perf): record S3-A measurements`。 + +### 4.6 风险 + +- 手动 RC 纪律扩大 invariant-panic 面:debug 构建维持全量 generation 校验 + + 冻结向量兜底;trusted 访问器遇 stale 即 panic 的政策不变(S1 已确立)。 +- 触及面最大(值类型是所有模块的公共依赖);必须与 B/D 分阶段,不可一锅端。 + +## 5. B:quickening + 可变执行 IR(差异化武器) + +### 5.1 设计 + +把「规范指令」与「执行指令」分离: + +- `code: Rc<[Instruction]>` 保持不可变、已验证、BC5 契约不变 + (`code/bytecode.rs:111-114` 的单一指令契约注释、验证器 + `code/bytecode_validation.rs`、二进制对象链路均不动); +- 每个 `PublishedFunctionSnapshot` 旁挂一条**可变执行 IR**: + `quick: Box<[QuickOp]>`,8B/word(opcode u8 + 操作数位 + IC 槽号), + 发布时从规范指令译出; +- 运行时按 feedback 把通用 QuickOp 重写为特化形(`Add → AddInt`、 + `GetField → GetFieldIC`、比较/分支/调用同理),guard 失败 deopt 回通用形; + **发布时由验证器认证每个 PC 允许的重写集合**——与 `FusionPlan` 同一信任 + 模型,BC5 只含规范 opcode,quickening 是纯运行期层; +- 特化状态 per-FunctionBytecode 共享(与现有 IC 站点一致, + `code/executable.rs:267-269`),CPython 亦然。 + +### 5.2 为什么这是「超过 QuickJS」的点 + +- CPython(PEP 659)、JSC LLInt、V8 Ignition、Deegen 全部收敛到 + quickening + IC;**QuickJS 完全没有 type feedback**——算术、比较每次走 + 完整通用路径。给算术/比较/属性/调用装上带 guard 的特化 + deopt,是在 + QuickJS 的盲区建立结构优势(证据量级见附录 A.1–A.3:特化归属 10–25%+)。 +- 纯安全 Rust 可达:`Cell`/侧表模式已有先例,无需改 GC、无需 unsafe。 + +### 5.3 顺带解决的存量问题 + +- IC 槽号直接编码进指令字,干掉 GetField 的 bitmap + block-rank 站点查找 + (`object/property_ic.rs:364-416`); +- fusion 跨度直接译成单个 QuickOp(`code/fusion.rs` 的 span 模式平移); +- 指令 ~16B enum → 8B word,icache 占用减半; +- 198-variant 大 match(`run.rs:328-1832`)分为热集 + generic 两档。 + +### 5.4 分步提交 + +1. `feat(code): decode canonical instructions into quick ops at publish` —— + 执行 IR 只读译码 + 派发切到 QuickOp(语义不变, fusion 平移); +2. `feat(vm): quicken arithmetic and comparison ops with guards and deopt`; +3. `feat(vm): quicken property access with embedded ic slots`; +4. `feat(vm): quicken call sites`; +5. `docs(perf): record S3-B measurements`。 + +### 5.5 风险 + +- 验证面扩大:重写集合必须在发布时认证,deopt 必须回规范语义;pc2line / + `Ret`/`Gosub` 的 pc-as-`Value::Int` ABI(`run.rs:1773-1796`)以规范指令 + 索引为准,QuickOp 侧维护映射; +- 冻结向量是最强兜底;每个 quickened opcode 需配 guard-fail 单测。 + +## 6. D:数据导向堆布局(与 A 复利) + +按收益密度排序,各项独立成 PR: + +1. **Typed arenas**:440B 统一 `ArenaSlot` 拆 per-kind arena(Object / VarRef / + Shape / Context / FunctionBytecode 各自 `Vec` + 各自 free list)。句柄格式 + 不变,trusted 访问器平移。VarRef/Shape 槽从 440B 降到几十 B;局部性与 + RSS 一起改善。 +2. **对象内联属性槽**:`slots` 改 SmallVec 模式(前 2–4 槽内联,溢出再堆 + 分配),消灭每个对象一次独立分配。 +3. **Per-prototype validity cell 取代全局 `property_layout_epoch`**: + `used_as_prototype` 对象各自携带 epoch/cell;IC 条目记录具体 cell。 + 现状是任一 proto 写全堆杀 depth>0 IC,原型链密集负载(deltablue / + richards 类)白丢缓存。 +4. **atom / string 便宜化**:`StringRepr` 头部缓存 hash;atom 字符串表 + SipHash → FxHash(`src/engine/hash.rs` 已有);shape 迁移改 per-shape 小 + Vec(1–2 项内联)替代 runtime 全局嵌套 HashMap。 +5. **S2.1 快速释放单独立项**:A 落地后重新评估「任何借用都 defer」的 + 契约松弛(`src/engine/heap/slot_ownership.rs:208` 测试钉死),不作为 + 性能 PR 的附带改动。 + +## 7. C:派发与栈流量——收割,不重写 + +- **不做寄存器式重写**:fat-opcode JS 的现代证据只有 ~7%(附录 A.5), + 对不起 198 variant + 验证器 + 挂起 ABI(`vm/suspend.rs`)的全面翻新。 + locals/args 本就是直寻址槽(等价寄存器),目标只剩临时栈流量。 +- **TOS/accumulator 缓存**:栈顶 1 项驻留;二元运算/比较/分支原地 peek + (`binary_number_current` 已是先例,`run.rs:1603-1663`),推及比较/存储。 +- **扩展静态超指令**:按 profile 选热对,在 QuickOp 层融合(B 的 IR 令其 + 成本骤降)。现代 ITTAGE 下期望 5–15%,不是 2003 年的 3×。 +- **派发机制**:保留单点 match 为基线(附录 A.6:现代预测器下单点 switch + 并不差);handler 抽到宏背后以便 A/B fn-pointer threading。stable Rust + **不保证** tail call(wasmi 赌 LLVM TCO + 逐版本审汇编);Rust 1.92 的 + DestinationPropagation 曾合并条件分支站点致同类解释器 −30~50%—— + **每次升 rustc 必须数二进制里的间接跳转**。此项是选项不是地基,最后做。 + +## 8. F:受审计 unsafe 保留席位 + +A–E 全部不需要 unsafe。仅当落地后 profile 点名具体位置(trusted 访问器的 +bounds check、PC 裸指针化、侵入式链表)时,按 `parity.md:23` 逐点引入。 +引入即治理动作:翻 `unsafe_code = "forbid"`(`Cargo.toml:46-47`)、收窄 +codec 自测门禁(`status.md:319-320`)、修订 `status.md:3` 的 "unsafe-free" +声明——三件事必须显性完成,不得在性能 PR 里夹带。 + +## 9. 不采纳的路线与理由 + +| 路线 | 理由 | +| --- | --- | +| 寄存器式 VM 全面重写 | fat-opcode JS 实测 ~1.067×(附录 A.5);重写成本巨大 | +| tracing GC 替代 RC | **推迟至 S4(见 §10),非否决**:§13 契约当前排除;S3 内只降 RC 单价(A/D) | +| nightly `become` / musttail | MSRV 1.88 stable;x86 codegen 仍不稳定(附录 A.4) | +| copy-and-patch / 动态复制 | 运行期机器码生成,属 JIT(约束 1) | +| wasm-as-interpreter-format | 是换产品形态,不是解释器技术 | +| Nova 式 8B enum(boxed f64) | 浮点上堆,算术密集路径引入分配;索引 NaN-box 更优 | + +## 10. S4 候选:RC → tracing GC(推迟,非否决) + +### 10.1 定位 + +- **不是当前差距的约束项**:QuickJS 同为 RC + 循环回收,仍快本项目 + 10–25×(`performance-plan.md:319-320`)——RC 不是天花板,值表示 / 派发 / + IC / 对象布局才是。S0–S2 实测 RC 直接成本 ≈ `copy_value` ~8% + release + 路径 ~4%(S1 后更低)。 +- **是 S3 之后的最大单项杠杆**:S3 削掉其他项后,「每次值复制/销毁各一次 + 计数」的语义性开销将浮为头部项——RC 是一块地板,tracing 是拆地板的 + 唯一手段。 +- **与方案 A 不冲突**:A 的 8B 句柄值 / 索引 NaN-box / typed arena / 边界 + root 层在 tracing 下约 80% 原样复用;被废弃的只有「显式 dup/release + 纪律」一层。先做 A 在任何未来路径上都不亏。 + +### 10.2 若实施的设计形态 + +- **保留**:typed arena;`{index, generation}` 句柄;`Edges` 出边遍历 + (循环回收器已有,mark 阶段直接复用);边界 root 类型。 +- **删除**:`strong` 计数、zero_queue、deferred 队列、trial-deletion 循环 + 收集器、全部 retain/release、所有「drop 改堆」逻辑。 +- **新增**: + - per-slot mark 位(或侧 bitmap)+ mark stack + sweep 重建 free list; + - **Root registry**:边界 root 类型从「Rc + 计数」改为「registry 条目 + + Drop 注销」,仅跨 GC 点持有的代码创建——tracing 的结构性优势即在此: + RC 在每条内部路径按次计费(操作数栈、帧、对象字段、常量池),tracing + 只在 native 边界收注册费,内部流量全免; + - **safepoint 纪律**:GC 仅发生在分配点与 operation 边界;「两个分配点 + 之间持有的裸句柄必须活在 VM 可扫描状态(操作数栈/帧)」成为不变量; + - 分代化时的 write barrier:老→新字段写 push remembered set(仅堆字段 + 写收费); + - **debug GC stress 模式**:每次分配即回收,配合 generation 校验把 + rooting 错误从「事后悬垂」变成「即时 panic」。 +- **架构简化红利**:drop-defer 契约网整体蒸发(S2.1 类冲突不复存在)、 + Rc + heap 双计数消失、retain-during-borrow panic 类别消失、`Value` 变 + `Copy`、操作数栈操作退化为数组搬移。 + +### 10.3 安全 Rust 下 rooting 纪律的三条路线 + +| 路线 | 结论 | +| --- | --- | +| gc-arena(不变量生命周期) | 完全安全证明,但堆访问闭包域化 ≈ 强制 stackless 解释器,等于全引擎重写——否决 | +| Nova 式 reborrow | 编译期强制 rooting,但 ~800 个 bind/unbind 站点、人体工学差,作者自述 soundness 仍在研究——不选 | +| **显式 root registry**(SpiderMonkey `Rooted` / V8 `HandleScope` 同族) | **选中**:纪律性风险用 generation 校验 + stress 模式兜底;是「运行时抓」而非「编译期消灭」,接受这一点 | + +### 10.4 成本与风险(诚实清单) + +- rooting 纪律是全 codebase 级人因风险(忘 root = 提前回收); +- **契约谈判是真正门槛**:§13 钉的是可观察的确定性回收——临时对象立即 + 释放、FinalizationRegistry/WeakRef 时机、内存 footprint 行为;换 tracing + 后对象死于 GC 时刻,冻结向量需逐案重审、`parity.md` 需修订——这是产品 + 主人的决定,不是性能 PR; +- 内存 headroom:tracing 不能像 RC 在接近满堆时运行;分代 minor GC 控制 + 暂停; +- write barrier 常驻税约几个百分点; +- 工期估计与方案 A 同级或更大。 + +### 10.5 中间路线(不动 §13,先拿走 RC 税的大块) + +1. pinned atom / interned string 全体 immortal 化(`u32::MAX` 饱和先例已有); +2. consume 点 move-not-copy 纪律 + 直线代码 retain/release 配对消除; +3. S2.1 快速释放单独立项(§6 第 5 项)。 + +中间路线落地后**必须重新量化** tracing 的边际收益——届时残余 RC 成本可能 +只剩个位数百分比,S4 未必仍值得。 + +### 10.6 决策门禁(两个同时满足才启动 S4) + +1. **量化门禁**:S3(E/A/B/D)落地且 §10.5 中间路线榨尽后,profile 归因于 + RC 机制的总成本(copy/retain/release/deferred/zero-queue/atom 计数)在 + 目标负载上仍 **> 15–20%**; +2. **契约门禁**:产品主人书面接受修订 `parity.md` §13; + FinalizationRegistry/WeakRef 时机语义重新审计;冻结向量重基线。 + +### 10.7 预期与证据 + +- S3 之上再 **1.3–2×**;分配/GC 密集负载(splay 类)数倍。 +- 证据:RC Immix(OOPSLA 2013,附录 A.8)显示优化后的 RC 可追平分代 + tracing——「RC vs tracing」的差距大半在实现质量而非原理;CPython 以 + PEP 683(immortal objects)/ PEP 703(biased refcounting)给 RC 本身做 + 手术,证明 RC 成本真实且值得重构。 + +## 11. 路线、预期与验证门禁 + +### 路线 + +E(重定基线)→ A(地基)→ B(差异化)→ D/C 按测量交替推进。每阶段 +独立可回退,严禁跨阶段混合提交。S4(RC → tracing GC)不在此路线内,按 +§10.6 双门禁另行决策。 + +### 预期(诚实口径) + +- 累积约 **2.5–4×** vs PGO 后基线。对 QuickJS 的 10–25× microbench 差距 + (`performance-plan.md:319-320`)大概率收敛到**同数量级、仍差 2–5×**—— + 见附录 B 的税种分析。 +- 「超过 QuickJS」按轴成立:**算术/特化密集**(QuickJS 无 quickening)与 + **原型链/多态属性**(validity cell + 更深 IC vs QuickJS 静态快路径)可 + 反超;纯字符串/正则轴不奢望(表示层同源)。 +- 所有数字以方案 E 后的基线为分母;无 PGO 基线之前的对比数字不得引用。 + +### 验证门禁(每阶段) + +1. `cargo fmt --check`; +2. `cargo clippy --locked --all-targets -- -D warnings`(1.88); +3. `cargo test --locked --workspace --all-targets`; +4. Test262:`--check` → `--focused` → `--full` 零回归(运行前清理 `GIT_*` + 环境变量);只产出 current-source receipt,**不改 `current.conf`**; +5. `python3 scripts/checks/check-source-layout.py` + rust-only 门禁; +6. 基准:`property_read_probe.py` + `scaling.py` + `run.py`(v8-v7 / + microbench),串行、独立输出目录、receipts 齐全。**比较协议**:开工前 + 保存一份无 PGO、无 LTO 的固定基线;每阶段只与上一阶段和该基线比较 + (两侧同 flags、无 PGO/LTO);大阶段收尾建议(非强制)一次 LTO+PGO + 双方复核;跨协议对比须标注双方构建协议(细则见 + `scripts/benchmark/README.md`「Profile-guided optimization」与 + `s3-a-plan.md` §2); +7. profiling 构建核对计数器无异常漂移; +8. 每阶段结束更新本文档对应章节的实测结果。 + +--- + +## 附录 A:外部证据 + +### A.1 CPython 特化解释器(PEP 659,3.11–3.14) + +- 机制:字节码可变,通用指令执行 ~8 次后自重写为特化形并内嵌 IC;guard + 失败经饱和计数 deopt 回自适应形。 +- 速度:3.11 官方口径 geomean 1.25× vs 3.10(捆绑了零成本异常与调用改造, + **无干净的 PEP 659 单独归属**);PEP 自估特化贡献 10–60% 区间、 + super-instruction 另有一小部分。 +- 重要旁证:3.13 的 copy-and-patch JIT 初期只有 2–9%——**特化解释器已经 + 拿走了可达收益的大头**。 +- 经验:单态缓存优先于多态(简单且交替类型回通用形可接受);deopt 必须 + 是单指令重写,不做区域 bailout。 + +### A.2 Deegen / LuaJIT Remake + +- 元编译器:从 C++ 语义描述自动生成 CPS tail-call 解释器 + 自动 quickening + 变体 + 自动 IC。其生成的解释器 geomean 比 LuaJIT 手写汇编解释器快 + ~28–31%(34 项赢 31;**注意当时 GC 未实现、对比关了 LuaJIT 的 GC**)。 + ;论文版 OOPSLA 2024 + +- 可借鉴(无需元编译器):IC 与 opcode 融合以去掉命中路径的间接跳转; + hot/cold 变体分裂;「缓存幂等步、重放效果步」。 +- 不可借鉴:GHC calling convention、IR 级原型统一——需要身在 LLVM 内。 + +### A.3 学术脉络 + +- Brunthaler quickening 论文称至 5.5×,但屡被拒稿、口径是 microbenchmark + 上限,只作方向参考。 +- Shannon thesis(Glasgow 2011)是 PEP 659 的设计基础。 + + +### A.4 tail-call / CPS 派发 + +- Wasm3 Massey 模型 + Steven Johnson 的 musttail 系统化: + + +- 现状:Clang/GCC 已有 musttail;**stable Rust 无 tail-call 保证**,`become` + 仅 nightly 且 x86 codegen 有已知问题(ARM64 上安全 Rust tail-call 解释器 + 已能胜手写汇编)。 +- CPython 3.14 tail-call 解释器 headline 9–15%,经 Nelson Elhage 复查大部分 + 是绕过 LLVM 19 回归;**对好基线的真实收益 1–5%**,复制派发本身在现代 + 核上 ~2%。 +- Wasmtime Pulley A/B:真实负载上 giant match 与 tail-call 互有胜负 + (bz2 上 match 快 13–19%,spidermonkey 上 tail-call 快 3–4%)。 + +- wasmi 2.0:四种派发模式并存(direct-threaded 最快,indirect-threaded 慢 + 10–15% 但 IR 小),整体 2.2×;教训含 Rust 1.92 DestinationPropagation + 合并分支站点事件。 + +### A.5 寄存器 vs 栈式 + +- 经典:Shi/Casey/Ertl/Gregg(VEE'05 + TACO 2008)寄存器机静态指令少 + 43%、时间少 ~26.5%——但那是 thin-opcode JVM。 + +- 现代再评估:RegCPython(ACM TACO 2022)平均仅 **1.067×**(最好 1.287, + 最差 0.977)——fat-opcode 高级语言上派发占比小,省派发买不到多少。 + +- 折中甜点位:Ignition 的「寄存器文件 + 隐式 accumulator」 + 与 wasmi 的 `ireg`/`freg` + 累加器——TOS 缓存拿到大部分收益而无需格式革命。stack caching 原始文献 + Ertl PLDI 1995「栈顶 1 项驻留通常最优」。 + +### A.6 分支预测时代 + +- Ertl & Gregg PLDI 2003:老 BTB 上 switch 派发误预测 81–98%,复制 + + 动态超指令至 3.17×——硬件已变。 + +- Rohou/Swamy/Seznec CGO 2015「Don't Trust Folklore」:TAGE/ITTAGE 级 + 预测器下,单点 switch 与复制 threaded 几乎打平。 + +- 结论:派发机制微调的期望是个位数百分比;减少派发次数(quickening、 + 超指令、TOS 缓存)比优化单次派发更值。 + +### A.7 构建层 + +- BOLT:FDO+LTO 之上再 ~8%(无 FDO/LTO 时至 20.4%),HHVM 实测 ~7%。 + +- V8 pointer compression 博客(32-bit 句柄/偏移的内存与速度收益:堆至 + −43%,CPU/GC 5–10%)——arena 索引是同一思想的安全版本。 + + +### A.8 值表示与 GC 的 Rust 先例 + +- Nova(安全 Rust ECMAScript 引擎):值 = 内联标量或 u32 句柄的 enum, + 堆 = per-type arena,GC 时 compact 提局部性。 +- Kiesel(Zig):16B→8B NaN-boxing。 + +- **未发现任何生产引擎 NaN-box arena 索引**(均为 box 指针):本方案的 + 8B 编码是合理但无生产先例的设计,收益/成本(每次 deref 多 base load + + bounds check)须自行测量——这正是方案 A 要求先建基线的原因。 +- RC vs tracing:RC Immix(OOPSLA 2013)在 JVM/MMTk 上追平分代 Immix, + 但解释器里每次计数操作都是软件开销,无公开数据覆盖「索引 arena + 安全 + Rust」场景;本项目 S3 内由契约排除 tracing(S4 候选见 §10),此比较 + 仅作背景。 + + +## 附录 B:为什么 C 的常数与安全 Rust 的税不会全消失 + +把差距拆成三类税,结论各异: + +1. **自残税(可全消)**:32B 值、SipHash、全局 epoch、440B 混排 slot、 + `Result` 管道——这些与安全无关,是历史实现的债务。S0–S2 已证明这类可 + 逐项消除,S3 的 A/D 继续。 +2. **安全税(可摊薄,不可归零)**: + - 每次堆解引用 = base load + bounds check(arena 下标),C 的 + `ptr->field` 是单条 load;解释器的下标来自字节码操作数,编译器基本 + 无法证明范围,消除率低; + - `RefCell`/借检查把「单线程可变性」从 C 的编译期(程序员纪律)挪到 + 运行期(flag 读改写); + - 不可用的工具:computed goto、musttail、手写汇编派发(rust-only 门禁)、 + `get_unchecked`(政策保留)——各值个位数百分比,累加即税。 +3. **契约税(结构性)**: + - RC 次数:§13 钉死确定性 RC + 循环回收,值每次复制/销毁各一次计数。 + 对 V8(tracing,复制纯 mov)是天花板;对 QuickJS 是同税——但 QuickJS + 的计数是对象头内 `ptr->ref_count++`,本项目是 arena 查槽 + `Cell` + 读改写 + 状态机分支,**同税不同价**,方案 A/D 在缩价差; + - 健壮性门禁(`parity.md:77`:OOM/栈限/中断不 panic)要求边界保留 + 可失败路径与回退分支,QuickJS 在同位置直接返回 NULL/longjmp,路径 + 更短。 + + 量级:同设计下典型残留 **10–30%** 常数差。 +4. **反向项**:安全换来的是激进重构不穿帮——quickening、IR 重写、typed + arena 在 C 里是高危手术,在这里由类型系统兜底。税是常数项,设计收益 + 是结构项;这正是「分轴反超」(§11 预期)的根据。 diff --git a/docs/reports/performance-plan.md b/docs/reports/performance-plan.md new file mode 100644 index 00000000..e3f0fe69 --- /dev/null +++ b/docs/reports/performance-plan.md @@ -0,0 +1,688 @@ +# 性能改造计划(heap/value fast path) + +本文件记录面向 QuickJS 的常数因子改造:计划、决策、实现与度量。各阶段独立 +可验证、可回退,结果随实现更新。 + +- **S0**:属性读路径成本测量(证据见本文件 §2「热点路径与证据」;负载由 `scripts/benchmark/property_read_probe.py` 复现) +- **S1**:可信快路(trusted fast path)——已实现 +- **S2**:可信路径收尾 + 快速释放——部分实现(S2.2;S2.1/S2.3 因契约冲突撤销) +- **S3 及之后**:横向设计比较(见文末「横向设计比较」)→ S3 已定稿为 + `docs/reports/performance-architecture.md`(8B 值表示 + quickening + 数据导向堆,无 JIT、默认无 unsafe) + +--- + +# S1 计划:可信快路(trusted fast path) + +> 状态:已实现。下方保留实施前的原始计划,实测结果见本文件「S1 第 9 节」。 + +S1 只改**热路径的开销**,不改任何 JS 可观察行为,也不换 GC 模型(继续 +plain RC + 循环回收)。 + +## 0. 已决定的取舍 + +| 项 | 决定 | +| --- | --- | +| 可信快路遇到 stale handle / 内部哨兵 | **panic**(`debug_assert` + release panic),视为不变量被破坏的 bug | +| 可信快路的 refcount 溢出 | **饱和为 immortal**(`u32::MAX`),不返回错误 | +| 现有可失败 `retain_*` 的溢出 | **保持返回 `Err`**,签名与语义不变(不动现有溢出测试) | +| S1b(Symbol atom `Cell` 化、`VarRefData.value` `RefCell` 化) | **留到 S1 验证后再做** | + +关键点:**快路与通用可失败路径的溢出行为不同**——快路饱和、通用路径报错。 +快路只用于计数很小的已证明存活句柄,饱和在实践中不可达,因此 +`src/engine/heap/tests.rs` 里 `retain_edges_transactionally` 的溢出用例无需改动。 + +## 1. 目标与非目标 + +**目标**:消除 S0 定位到的三大开销来源,即热路径上的 + +1. `Result` / `Option` 管道分支; +2. 全局 `RefCell` 的**可变**借用(`try_borrow_mut`); +3. 可避免的 `validate_slot_identity` 校验。 + +**非目标**: + +- 不删除 `Value` 的 runtime `Rc`(属于 S3 值瘦身)。 +- 不改 GC 回收时机(保持即时 RC)。 +- 不为 public API 或未可信输入放宽错误返回。 +- 不改 Symbol 的 atom 表可变借用(S1 走回退路径,见 4.5)。 +- 不改现有可失败接口的签名与失败语义。 + +## 2. 热点路径与证据(来自 S0) + +S0 测得 `prop_read_int` 的一次循环迭代: + +| 步骤 | 调用链 | 成本 | +| --- | --- | ---: | +| 读捕获对象 `o` | `read_run_cell` → `read_immediate_cell`(shared,decline) → `try_read_owned_var_ref`(`try_borrow_mut`) | 6.47%(其中 `Result::map` 6.29%) | +| 读属性 `o.a` | `stack::property_ic_read_current` → `try_property_ic_read_owned`(`try_borrow_mut`) | int 11.36% / obj 24.98%(`Result::branch` 12.54%) | +| 写立即数 `s`/`i` | `try_write_immediate_cell` → `try_replace_immediate_var_ref_value`(`try_borrow_mut`) | 6.92% | +| `run` 内联 | `Result::branch` + `copy_value` | 14.85% + 7.96% | + +结论:主导成本是 **`Result`/`Option` 分支**,其次是可变借用与校验,而不是 +引用计数本身。 + +## 3. 根因 + +热路径每一步都走可失败、需要 `&mut` 的通用接口: + +- `Heap::live_node` / `live_node_mut` / `validate_slot_identity` 返回 `Result`; +- `Node.strong` 是裸 `u32`,retain 需要 `&mut`; +- `Runtime::retain_raw_root` 返回 `Result`(Object 走 heap retain,Symbol 走 atom retain); +- `Runtime::take_owned_raw_value` 返回 `Result`(拒绝内部哨兵); +- 于是 `try_*` 层层用 `Result>` 包裹,每次调用一个分支。 + +这些 `Result` 绝大多数是**防御性不变量**(stale handle / refcount overflow / +内部哨兵),在「句柄已由活对象持有」的热路径上不可能发生。 + +## 4. 设计:可信快路 + +核心原则:**内部已证明存活的句柄走不失败、可共享借用的快路;通用可失败 +接口保留给边界与冷回退。** + +### 4.1 refcount 改为 `Cell` + 新增快路 retain + +- `Node { strong: u32, data }` → `Node { strong: Cell, data }` + (`src/engine/heap/mod.rs`),`SlotState::strong()` 用 `.get()`。 +- **现有** `Heap::retain_raw(&mut self, id, additional) -> Result` 及其所有 caller + **签名与错误语义不变**,只把字段读写改成 `.get()/.set()`。溢出仍返回 `Err` + (保住 `retain_edges_transactionally` 的现有测试)。 +- **新增** 快路: + ```rust + #[inline] + pub(in crate::engine::heap) fn retain_raw_fast(&self, id: RawId) { + let node = self.live_node_fast(id); + node.strong.set(node.strong.get().saturating_add(1)); + } + ``` + 仅供快路使用,饱和后不可回退(immortal)。 +- release 的「归零入队」仍需 `&mut`,`release_raw_no_drain(&mut self)` 保持不变; + release 读/写 `Cell` 用 `.get()/.set()`,`u32::MAX` 视为 immortal(不递减)。 +- 更新全部 `.strong` 直接字段访问(`gc.rs`、`arena.rs`、`slot_ownership.rs`、 + `roots.rs`、`mod.rs`、`tests.rs`,约 32 处)。 + +### 4.2 可信访问器(无 `Result`、省 generation 校验) + +在 `Heap` 上新增仅供**内部已证明存活句柄**使用的访问器: + +```rust +impl Heap { + /// Trusted: caller holds a live owning edge. Bounds-checked index; the + /// generation/state check runs in debug builds only. + #[inline] + pub(in crate::engine::heap) fn live_node_fast(&self, id: RawId) -> &Node { + debug_assert!(self.validate_slot_identity(id).is_ok()); + match &self.slots[id.index() as usize].state { + SlotState::Live(node) => node, + _ => unreachable!("trusted handle reached a non-live slot"), + } + } + #[inline] + pub(in crate::engine::heap) fn var_ref_fast(&self, id: VarRefId) -> &VarRefData { ... } + #[inline] + pub(in crate::engine::heap) fn object_fast(&self, id: ObjectId) -> &ObjectData { ... } +} +``` + +保留现有 `live_node`/`validate_slot_identity` 作为边界与测试路径。注意 release +构建仍会做 `Vec` 越界检查(安全 Rust,不用 `get_unchecked`)。 + +### 4.3 无失败根转换 + +- 新增 `Runtime::take_owned_raw_value_fast(&self, raw: RawValue) -> Value`: + 仅接受公开变体(Undefined/Null/Bool/Int/Float/BigInt/String/Symbol/Object), + 内部哨兵走 `debug_assert` + panic。逻辑复制自 `src/engine/heap/roots.rs:345`。 +- 新增 `Runtime::retain_object_root_fast(&self, id: ObjectId)`:调用于共享借用 + 下的 `heap.retain_raw_fast(RawId::Object(id))`,不返回 `Result`。 + +### 4.4 快路函数与调用点改写 + +新增(原函数保留为冷回退): + +- `Runtime::read_owned_cell_fast(&self, root) -> Option` + - `state.try_borrow()`(共享); + - `heap.var_ref_fast` 读单元; + - 仅处理 Object/String/BigInt(String/BigInt 克隆自带所有权,Object 用 + `retain_object_root_fast`); + - Symbol 与标量 → `None`,回退 `try_read_owned_var_ref`。 +- `bindings::read_run_cell_fast(runtime, root) -> Option` + - `read_immediate_cell`(现有,shared)→ else `read_owned_cell_fast`。 +- `Runtime::property_ic_read_fast(base, executable, pc, key, keep_receiver, native) -> Option` + - 复制 `try_property_ic_read_owned`(`ic.rs:12`)的**数据属性命中**分支: + - 共享借用;`heap.object_fast` / `cache.read` / `slot_object_release_readiness` 均 `&self`; + - Object/String/BigInt 用可信 retain; + - `keep_receiver` / native 选择 / accessor / 描述符等情形 → `None` 回退原函数。 + +调用点改写: + +- `src/engine/vm/bindings.rs:64` `read_run_cell`:先 `read_immediate_cell`,再 + `read_owned_cell_fast`,最后才回退 `try_read_owned_var_ref`;不再包 `Result`。 +- `src/engine/vm/run.rs` 的 `GetVarRef`/`GetArg` captured 分支改用 + `read_run_cell_fast`,去掉 `?`。 +- `src/engine/vm/stack.rs:114` 与 `src/engine/object/ordinary_storage/ic.rs` + 的属性读调用点:先 `property_ic_read_fast`,`None` 再走原可失败路径。 +- `try_replace_immediate_var_ref_value` 保持 `bool`;删除其中与前置判断重复的 + `validate_var_ref_value` 调用,并用 `var_ref_fast_mut`(新增,可信 `&mut`)。 + +### 4.5 Symbol / 立即写的原因与边界 + +- **Symbol**:`AtomTable::retain` 需要 `&mut`(atom 表可变),无法在共享借用 + 下完成。S1 让 Symbol 读回退到 `try_read_owned_var_ref`(S1b 可把 atom 的 + `ref_count` 也 `Cell` 化)。 +- **立即写**(`i`/`s`):要修改 `VarRefData.value`(`RawValue`,非 `Copy`), + 仍需要 `&mut`。S1 只降低其校验与 `Result` 开销,不消除可变借用;彻底消除 + 需要把单元值改成 `RefCell`(S1b)。 + +## 5. Scope 清单 + +| 文件 | 改动 | +| --- | --- | +| `src/engine/heap/mod.rs` | `Node.strong: Cell`;`SlotState::strong` | +| `src/engine/heap/gc.rs` | `.strong.get()/.set()`;新增 `retain_raw_fast` | +| `src/engine/heap/arena.rs` | 新增 `live_node_fast` / `var_ref_fast_mut` 等可信访问器 | +| `src/engine/heap/slot_ownership.rs` | `.strong.get()` | +| `src/engine/heap/roots.rs` | 新增 `take_owned_raw_value_fast`、`read_owned_cell_fast` | +| `src/engine/heap/binding_storage.rs` | 精简 `try_replace_immediate_var_ref_value`;`var_ref_fast_mut` | +| `src/engine/heap/runtime/mod.rs` | 新增 `retain_object_root_fast` | +| `src/engine/vm/bindings.rs` | 新增 `read_run_cell_fast` | +| `src/engine/vm/run.rs` | captured 读调用点改快路 | +| `src/engine/vm/stack.rs` | 属性读调用点改快路 | +| `src/engine/object/ordinary_storage/ic.rs` | 新增 `property_ic_read_fast` | +| `src/engine/heap/tests.rs` | `.strong` 测试适配 | + +预计 12 个源文件,不新增模块,不改 public API。 + +## 6. 算法草图 + +```rust +// heap/gc.rs +#[inline] +pub(in crate::engine::heap) fn retain_raw_fast(&self, id: RawId) { + let node = self.live_node_fast(id); + node.strong.set(node.strong.get().saturating_add(1)); +} + +// heap/roots.rs +#[inline] +pub(crate) fn read_owned_cell_fast(&self, root: &VarRefRoot) -> Option { + if !root.belongs_to(self) || self.0.deferred_references.has_pending() { + return None; + } + let state = self.0.state.try_borrow().ok()?; // shared, 非 mut + if !state.heap.zero_queue.is_empty() { return None; } + let cell = state.heap.var_ref_fast(root.id()); + match &cell.value { + RawValue::Object(object) => { + state.heap.retain_raw_fast(RawId::Object(*object)); + Some(Value::Object(ObjectRef::from_owned_handle(self.clone(), *object))) + } + RawValue::String(s) => Some(Value::String(s.clone())), + RawValue::BigInt(b) => Some(Value::BigInt(b.clone())), + _ => None, // Symbol/标量回退 + } +} + +// object/ordinary_storage/ic.rs(数据属性命中分支) +#[inline] +pub(crate) fn property_ic_read_fast( + &self, base: &Value, executable: &PublishedFunctionSnapshot, + pc: usize, key: u32, native: &mut Option, +) -> Option { + let atom = linked_field_atom(self, executable, key)?; + let Value::Object(object) = base else { return None }; + if !object.belongs_to(self) { return None; } + let cache = executable.property_read_ic.site(pc)?; + let state = self.0.state.try_borrow().ok()?; + if state.heap.has_pending_zero_cleanup() { return None; } + let receiver = object.object_id(); + let raw = cache.read(&state.heap, self.domain_id(), executable.realm, receiver)?.clone(); + match raw { + RawValue::Object(id) => { + state.heap.retain_raw_fast(RawId::Object(id)); + Some(Value::Object(ObjectRef::from_owned_handle(self.clone(), id))) + } + RawValue::String(s) => Some(Value::String(s)), // Rc clone 已在 clone() 中 + RawValue::BigInt(b) => Some(Value::BigInt(b)), + _ => None, + } +} +``` + +## 7. 分步提交计划 + +1. `perf(heap): make refcounts Cell-backed` —— 仅 4.1 的字段机械化改造 + + 新增 `retain_raw_fast`,行为不变,测试通过。 +2. `perf(heap): add trusted non-fallible accessors` —— 4.2/4.3,未被调用, + 行为不变。 +3. `perf(vm): add shared-borrow fast paths for captured reads` —— + `read_owned_cell_fast` / `read_run_cell_fast` + `run.rs` 调用点。 +4. `perf(object): add property-read fast path` —— `property_ic_read_fast` + + `stack.rs`/`ic.rs` 调用点。 +5. `perf(heap): slim immediate var-ref write validation` —— + `try_replace_immediate_var_ref_value` 精简 + `var_ref_fast_mut`。 +6. `docs(perf): record S1 measurements` —— 更新 S1 报告数据。 + +每步独立可编译、可测;若某步无收益可单独回退。 + +## 8. 验证与度量 + +1. `cargo fmt --check`。 +2. `cargo clippy --locked --all-targets -- -D warnings`。 +3. `cargo test --locked --workspace`。 +4. Test262 冻结向量:`pass=79982 / eligible=80032 / total=102037` **不得倒退**。 +5. `python3 scripts/checks/check-source-layout.py` 及 rust-only 门禁。 +6. 重跑 `scripts/benchmark/property_read_probe.py`,与 S0 对比。 +7. profiling 构建对比 `heap_root_copies`,确认复制次数未异常变化。 + +## 9. 实施结果 + +已按本计划实现(`Cell` refcount、可信访问器、无失败根转换、captured/属性读快路、 +立即写校验精简)。行为不变。 + +### 计时对比(`property_read_probe.py`,N=5,000,000,median,同机) + +| case | engine | S0 ns/op | S1 ns/op | 变化 | +| --- | --- | ---: | ---: | ---: | +| prop_read_int | plain | 222.55 | 189.95 | −14.6% | +| prop_read_obj | plain | 270.94 | 243.62 | −10.1% | +| prop_read_string | plain | 299.68 | 273.83 | −8.6% | +| prop_read_int | profiling | 343.98 | 309.44 | −10.0% | +| prop_read_obj | profiling | 409.00 | 355.74 | −13.0% | +| prop_read_string | profiling | 437.11 | 406.46 | −7.0% | + +### perf 归属变化 + +- `run::run` 自耗时从 42.50% 降到 36.29%(int)/ 33.99% 降到 29.13%(obj)。 +- 原 `try_property_ic_read_owned` 的 `Result::branch` 主导项消失;快路内联后 + 归属到 `RunSlots::property_ic_read`,不再看到成片的 `Result::branch` 子项。 +- `try_replace_immediate_var_ref_value` 去掉重复 `validate_var_ref_value`。 + +### 完整性能对比(pre-S1 `34db437c` vs S1 `fe537951`) + +基线在独立 worktree 构建;两者使用**相同 flags 的普通 release**(无 debug、 +无 profiling),串行运行,无并发构建/测试。 + +**属性读探针(`property_read_probe.py`,N=5,000,000,repeat 7,median):** + +| case | before ns/op | after ns/op | 变化 | +| --- | ---: | ---: | ---: | +| prop_read_int | 222.87 | 185.38 | −16.8% | +| prop_read_obj | 279.78 | 234.58 | −16.2% | +| prop_read_string | 306.63 | 284.84 | −7.1% | + +**`scaling.py`(整进程 wall,16 case × 3 size = 48 单元,operations=32768, +repeat 3):** 中位 `after/before = 97.0%`(**−3.0%**),区间 −15% ~ +23% +(小负载启动噪声大)。稳定收益集中在 S1 直接命中的路径:`long-key` +−8~−13%、`map-churn` −4~−14%、`arguments` −7~−15%、`array-holey` +−3.5~−7.5%、`array-index` −10~−12%、`typed-index` −3~−10.5%、 +`mapped-arguments` −1~−9.5%、`scope` 多为 −6~−12%。`map-*`/`set*` 基本中性。 + +**QuickJS 官方 `microbench`(毫秒分辨率):** `prop_read`/`array_read`/ +`int_arith` 前后均为定值、无法分辨 ~10% 变化;仅用于标定与 QuickJS 的差距 +(对应项 QuickJS 约 10–25 倍快)。 + +**V8-v7(Score,越高越好,repeat 3):** + +| case | before | after | quickjs | after 变化 | +| --- | ---: | ---: | ---: | ---: | +| richards | 48.7 | 48.6 | 1378 | −0.2% | +| deltablue | 63.4 | 62.2 | 1259 | −1.9% | +| crypto | 60.9 | 61.8 | 1522 | +1.5% | +| raytrace | 94.2 | 93.6 | 2832 | −0.6% | +| earley-boyer | 117 | 117 | 3504 | 0% | +| regexp | 87.6 | 86.9 | 639 | −0.8% | +| splay | 316 | 313 | 5118 | −0.9% | +| navier-stokes | 254 | 262 | 3224 | +3.1% | + +V8-v7 整体**中性(±2%,噪声内)**:计算密集型套件中属性读/binding 不是主导。 + +**小结:** S1 是定向优化——目标路径提升 7–17%,混合整进程负载中位 −3%, +计算密集型套件中性;不是全局加速,符合计划定位。 + +### 验证 + +- `cargo test --locked --workspace --all-targets`:通过(lib 2278、oracle 907、 + CLI 32、rust-only 4、unsupported-diagnostics 6 等,0 失败)。 +- `cargo fmt`:通过。 +- clippy:本次改动未新增 lint;1.88/1.94 下报出的均为仓库既有 lint + (`collapsible_if`、`manual_is_multiple_of`、测试 cfg 的 unused/dead_code)。 +- **Test262 全量通过、零回归**:`TEST262_WORKERS=2 ./scripts/test262/test-test262.sh + --full` 得到 + `total=102037 pass=79982 fail=3580 unsupported=3530 skipped=18475`, + `runnable=80032`,门禁判定 `complete Test262 vector matches: + 79982 pass of 80032 eligible (102037 total) variants`,与冻结基线逐字节一致。 + 该次运行只产出 current-source receipt,**未修改 `current.conf`**(符合 README + 的“不得为性能改动修改基线”)。注:`prepare-test262.sh` 会拒绝任何 `GIT_*` + 环境变量,运行前需清理。 + +## 10. 预期与风险 + +- **预期**:消除读路径的 `Result` 分支与可变借用。S0 显示 `Result` 管道合计 + >30%,其中读路径占大部分;S1 目标是把其中可移除的部分拿掉,期望整体有 + 可测的正收益(量级待测,不承诺具体倍数)。 +- **风险**: + - 可信快路的 panic 可能暴露此前被静默返回错误的真实不变量 bug —— 由 + Test262 与 workspace 测试兜底。 + - 共享借用与活动 `borrow_mut` 冲突时快路返回 `None` 回退,行为与现状一致。 + - `property_ic_read_fast` 只覆盖数据属性命中;其余走原路径,正确性不受影响。 + +--- + +# S2 计划:可信路径收尾 + 快速释放 + +## S2.0 背景与重定义 + +原 S2 目标是「热路径去 generation 校验」,但 S1 之后该目标**已基本达成**: +release 构建下 `live_node_fast`/`object_fast`/`var_ref_fast` 已省掉 generation +校验,重采样里 `slot_ownership`(ready 校验)仅约 **0.2%**。 + +S1 后 `prop_read_int` 热点(debug 构建,`perf report --no-children`): + +| 符号 | 占比 | +| --- | ---: | +| `vm::run::run` | 36.26% | +| `RunSlots::property_ic_read` | 14.97% | +| `SlotStore::push_current` | 10.48% | +| `try_replace_immediate_var_ref_value` | 9.36% | +| `bindings::read_run_cell` | 7.75% | +| `run::binary` | 4.94% | +| `RunSlots::insert_copy` | 4.20% | +| `apply_deferred_operation` + `release_raw_no_drain` + `release_or_defer` | ~4.1% | +| `slot_ownership`(generation ready) | 0.18% | + +因此把 S2 重定义为**三个仍可执行、可度量的子项**,继续不动 GC 模型、不动值大小: + +- **S2.1 快速释放**:释放路径在共享借用下用 `Cell` 递减,避免大部分独占借用与 + generation 校验。 +- **S2.2 可信 IC 读**:`PropertyReadCache::read_location` 与 readiness 证明改用 + 可信访问器,去掉 IC 读里的 generation 校验。 +- **S2.3 非失败 operand push**:`push_current` 的不变量检查改为非失败,去掉 + `Result` 分支。 + +共享原则与 S1 一致:**可信路径对不变量破坏 panic;通用可失败路径保留。** + +## S2.1 快速释放 + +### 现状 + +`ObjectRef::drop` → `Runtime::release_object_handle`(`ownership.rs:68`)→ +`release_or_defer(DeferredRefOp::Object(id))`(`ownership.rs:44`): + +``` +try_borrow_mut(state) // 独占借用整个 runtime +→ apply_deferred_operation + → release_heap_reference + → heap.release_object + → release_raw_no_drain // validate_slot_identity(generation) +→ drain_deferred_references() +``` + +即每次释放都占独占借用、做 generation 校验;共享对象的递减本不需要这些。 + +### 设计 + +- 新增 `Heap::release_raw_fast(&self, id: RawId) -> bool`(`gc.rs`): + ```rust + /// Trusted: only decrements while another owner remains. Returns false when + /// the count is 1, leaving the zero transition to the ordinary fallible path. + #[inline] + pub(in crate::engine::heap) fn release_raw_fast(&self, id: RawId) -> bool { + let node = self.live_node_fast(id); + let current = node.strong.get(); + if current > 1 { + node.strong.set(current - 1); + true + } else { + false + } + } + ``` + (`live_node_fast` 在 release 下不校验 generation;`current == 0` 不会出现, + Live 蕴含 `strong >= 1`。) +- `Runtime::release_object_handle`(`ownership.rs`): + ```rust + pub(crate) fn release_object_handle(&self, id: ObjectId) { + if let Ok(state) = self.0.state.try_borrow() { + if state.heap.release_raw_fast(RawId::Object(id)) { + return; // count>1, 无归零清理 + } + } + self.release_or_defer(DeferredRefOp::Object(id)); // 只能 count==1 或借用失败 + } + ``` + `live_node_fast` 若遇到非 Live 会 panic(可信路径语义)。count==1 时不递减, + 交由 `release_or_defer` 正常递减归零入队,**不会重复递减**。 +- 对 `release_var_ref_handle` / `release_context_handle` / + `release_function_bytecode_handle` 同样处理(各自 `release_raw_fast(RawId::X)`)。 +- `Atom` 保持原路径(需要 atom 表可变借用)。 + +### Scope + +| 文件 | 改动 | +| --- | --- | +| `src/engine/heap/gc.rs` | 新增 `release_raw_fast` | +| `src/engine/heap/ownership.rs` | 4 个 `release_*_handle` 加快速分支 | + +估计 ~40 行。 + +### 预期 + +削掉共享对象释放的独占借用 + generation 校验 + `apply_cleanup`。收益取决于负载中 +「引用计数 >1 的对象」比例(对象/数组共享多的负载更高)。`count==1` 的临时值仍 +走慢路,因此 **S2.1 不是普适加速**。 + +## S2.2 可信 IC 读 + +### 现状 + +`PropertyReadCache::read_location`(`object/property_ic.rs:74`)用可失败、带 +generation 校验的访问器: + +```rust +let object = heap.object(receiver).ok()?; // validate +let shape = heap.shape(object.shape).ok()?; // validate +// depth 循环里同样 object()/shape() +``` + +`property_ic_read_fast`(`ordinary_storage/ic.rs`)调用 +`slot_object_release_readiness`(`slot_ownership.rs:22`)→ `validate_slot_identity`。 + +### 设计 + +- 新增 `Heap::shape_fast(&self, id: ShapeId) -> &Shape`(`object_storage.rs`,紧邻 + `shape`),与 `object_fast` 同型:`debug_assert` 存活,非 Live panic。 +- `read_location` 改用 `object_fast`/`shape_fast`。命中路径的 `receiver` 与原型链 + `holder` 都是活对象(由活 receiver 可达,且 shape/revision/epoch 已判定匹配), + 可信。 +- 新增 `Heap::slot_release_readiness_fast(&self, id: RawId) -> SlotReleaseReadiness` + (`slot_ownership.rs`):去掉 `validate_slot_identity`,其余逻辑不变 + (zero_queue 检查 + strong 分支)。 +- `property_ic_read_fast` 改调 `slot_release_readiness_fast`。 + +### Scope + +| 文件 | 改动 | +| --- | --- | +| `src/engine/heap/object_storage.rs` | 新增 `shape_fast` | +| `src/engine/heap/slot_ownership.rs` | 新增 `slot_release_readiness_fast` | +| `src/engine/object/property_ic.rs` | `read_location` 改可信访问器 | +| `src/engine/object/ordinary_storage/ic.rs` | `property_ic_read_fast` 改调 fast readiness | + +估计 ~50 行。 + +### 风险 + +`read_location` 也被 Proxy trap 缓存等复用;这些调用点的 receiver 同样来自活值, +可信。若非可信调用者存在,保留原 `read` 走可失败路径即可(S2 只改热路径调用)。 + +## S2.3 非失败 operand push + +### 现状 + +`SlotStore::operand_push_index`(`vm/stack.rs:925`)返回 `Result`, +两个检查都是 VM 已验证的不变量(操作数容量、目标槽为空);`push_current`/ +`push_pending_current` 因此返回 `Result`,调用点用 `?`。 + +### 设计 + +- `operand_push_index` 改非失败: + ```rust + #[inline] + fn operand_push_index(&self, window: &FrameWindow) -> usize { + debug_assert!(window.depth < window.operands().len()); + debug_assert!(self.slots[index].is_none()); + ... + index + } + ``` + 越界/占位按不变量破坏 panic。 +- `push_current`/`push_pending_current` 去掉 `Result`;`insert_copy_current` 内部 + 调用相应调整;对外的 `push`(`vm/stack/window.rs:288/296`)保留 `Result` 或同步 + 改非失败(按调用者需要)。 +- 全仓库仅 5 个调用点,改动可控。 + +### Scope + +| 文件 | 改动 | +| --- | --- | +| `src/engine/vm/stack.rs` | `operand_push_index`/`push_current`/`push_pending_current` | +| `src/engine/vm/stack/window.rs` | `push`/`push_pending` 包装 | + +估计 ~60 行。 + +## S2.4 提交与验证 + +- 分 3 个 commit:`perf(heap): add fast shared release`、`perf(object): use trusted + accessors in the property-read cache`、`perf(vm): make operand pushes infallible`。 + 每步独立可编译/可测。 +- 验证:`cargo fmt`、workspace `--all-targets`、`TEST262_WORKERS=2 ... --full` + 零回归、`property_read_probe.py` + `scaling.py` 前后对比(pre-S2 vs S2)。 +- 老规矩:`prepare-test262.sh` 会拒绝 `GIT_*` 环境变量,需先清理。 + +## S2.5 风险与预期 + +- **失败模式**:与 S1 一致,可信路径把不变量破坏当 bug(panic)。 +- **预期量级**:S2.1 释放路径约 4%,S2.2 削 `property_ic_read` 的 15% 中的 + generation 部分,S2.3 削 `push_current` 的分支部分;合计**个位数百分比**。 + 诚实地说,剩余大头(`run` 36% + 值/槽表示)要靠 S3。 +- **S3(下一步)**:`Value` 从 32B 瘦身(去掉 runtime `Rc`、thin 句柄 + 显式 + retain,保持计数以免改 GC 根),目标 `push_current`/`insert_copy`/`copy_value`/ + drop 合计约 20% 与所有值搬运。 + +## S2.6 实施结果 + +实施中发现 **S2.1 与 S2.3 与既有契约冲突,已撤销**;只落地 **S2.2**。 + +- **S2.1 快速释放(撤销)**:既有测试 + `heap::slot_ownership::blocked_borrow_and_deferred_release_do_not_commit_or_drain` + 把「**任意**借用(含共享)都使释放 defer、不立即提交」固定为契约。快速释放用 + 共享借用递减,会在已持有共享借用时成功提交,改变该契约与 deferred 出队顺序。 + 需要单独立项评估该松弛是否安全,故本次不做。 +- **S2.3 非失败 push(撤销)**:`operand_push_index` 的容量/占位失败是**被显式 + 测试覆盖的可恢复事务路径**(`failed_capacity_and_shape_checks_do_not_change_live_windows`、 + 多个 `primitive_transaction_*` 测试)。改为 panic 会破坏该契约,故保持可失败。 +- **S2.2 可信 IC 读(已落地)**: + - 新增 `Heap::shape_fast`(`object_storage.rs`)。 + - `PropertyReadCache::read_location`(`object/property_ic.rs`)改用 + `object_fast`/`shape_fast`。 + - 新增 `Heap::slot_object_release_readiness_fast`(`slot_ownership.rs`), + `property_ic_read_fast` 改用,去掉读路径上的 `validate_slot_identity`。 + +### S2.2 计时(`property_read_probe.py`,N=5,000,000,repeat 7,median) + +| case | before ns/op | S1 ns/op | S2 ns/op | S1→S2 | before→S2 | +| --- | ---: | ---: | ---: | ---: | ---: | +| prop_read_int | 222.73 | 185.97 | 184.35 | −0.9% | −17.2% | +| prop_read_obj | 281.70 | 236.71 | 220.22 | **−7.0%** | **−21.8%** | +| prop_read_string | 300.87 | 280.10 | 280.86 | +0.3% | −6.7% | + +S2.2 的收益集中在**对象结果的属性读**(IC 命中里 object/shape 的可信访问 + +readiness 无校验);int/string 基本不变(噪声)。 + +### 验证 + +- `cargo test --locked -p quickjs-oxide --lib`:2259 通过。 +- `cargo test --locked --workspace --all-targets`:全部通过(lib 2278、oracle 907、 + CLI 32 等,0 失败)。 +- `cargo fmt`:通过。 +- Test262 全量零回归:`TEST262_WORKERS=2 ./scripts/test262/test-test262.sh --full` + 得 `total=102037 pass=79982 runnable=80032`,门禁判定 + `complete Test262 vector matches`,与冻结基线逐字节一致(仅产出 current-source + receipt,未改 `current.conf`)。 + +### 结论 + +S2.2 是安全的增量(对象属性读 S1→S2 −7%);S2.1/S2.3 的正确做法需要改动既有 +事务/延迟释放契约,应作为独立设计项,而不是塞进性能 PR。下一阶段(值表示 / +派发 / 自适应特化)见文末「横向设计比较」。 + +--- + +# 横向设计比较:参考引擎 vs quickjs-oxide + +> 原 S3「值表示瘦身」计划已移除,改为本横向比较,作为重新设计 S3 的依据。目标 +> 不是「补齐常数因子」,而是对齐/超过参考引擎的关键设计。 + +## 1. 总表 + +| 引擎 | 值表示 | 属性键 | GC / 回收 | 分配器 | 派发 | IC / 自适应特化 | JIT | +| --- | --- | --- | --- | --- | --- | --- | --- | +| **QuickJS** | **8B NaN-box** `JSValue` | `JSAtom` = 裸 `u32` | 侵入式 RC + 循环回收 | 自研 `js_malloc` | 栈式 + switch/threading | 少量静态快路径,**无 quickening** | 无 | +| **Lua 5.4** | 16B `TValue`(union+tag) | interned `TString*` 指针 | 增量/分代标记清除 | size-class | **寄存器式** | 无 | 无(LuaJIT 另立) | +| **LuaJIT** | 8B NaN-box | interned 指针 | tracing GC + RC | — | 解释器 + 汇编桩 | — | **tracing JIT** | +| **V8** | 8B / 32-bit 压缩 `Tagged` | `Name` 指针 | 分代 tracing(并发/增量) | bump nursery + size-class | **寄存器式(Ignition)** | feedback vector + 多态 IC | Sparkplug / Maglev / TurboFan | +| **JSC** | 指针 tagging | `Identifier`/`Name` | 分代 tracing | — | 手写汇编 LLInt + Baseline | 多态 IC | LLInt / Baseline / DFG / FTL | +| **SpiderMonkey** | 指针 tagging | `Name` | 分代 tracing | — | Baseline 解释器 | CacheIR | Warp | +| **CPython** | `PyObject*` 8B 指针 | 任意对象(interned `str`) | RC + 分代循环 GC | pymalloc / size-class | 栈式 | **PEP 659 特化 + IC** | 无(3.13 实验副本补丁,默认关) | +| **Boa** | Rust enum `JsValue`(~16B) | interner | `Gc` 标记清除 | — | 栈式 | 无 | 无 | +| **quickjs-oxide(现状)** | **32B enum**(实测) | `Atom` = raw+generation+table_id(16B) | arena RC + 循环回收 | arena `Vec` | 栈式 | mono/poly-2 IC + fusion,**无 quickening** | 无 | + +## 2. 逐维度要点与启示 + +### 值表示 +- 主流是 **8B**:QuickJS/LuaJIT 用 NaN-box;V8 用指针 tagging + 指针压缩;CPython 是 8B `PyObject*` 指针。**没有任何主流引擎用 32B 带标签枚举**(那是 Boa 与 quickjs-oxide 这类安全 Rust 实现的产物)。 +- 8B 的意义:一条 64B 缓存行放 8 个值(而非 2 个)、复制是一条 `mov`、可进 CPU 寄存器。 +- **quickjs-oxide 的 32B** 是最大差距;要追平 QuickJS,值表示必须降到 8B(NaN-box 或 thin 指针)。这需要 `unsafe`,与当前 `forbid(unsafe_code)` 冲突(`parity.md` 已允许受审计 `unsafe`)。 + +### 属性键 / atom +- QuickJS:裸 `u32`;Lua:interned 指针;V8/CPython:指针。**都无 per-handle 品牌**;有效性靠构造(per-runtime 表、不跨域)或 tracing(活对象不复用)。 +- **quickjs-oxide 的 16B `Atom`(raw + generation + table_id)** 是 Rust arena 安全的额外产物,比 QuickJS 每个键多 12 字节;shape entry、属性键内存同受其累。 +- 启示:去品牌 + 裸句柄能缩小键/shape,但只覆盖「值表示」的一个子维度;参考引擎靠「指针 + GC/构造」而非句柄品牌。 + +### GC / 分配 +- 回收:侵入式 RC + 循环回收(QuickJS)vs 分代 tracing(V8/JSC/SpiderMonkey)vs RC + 分代循环(CPython)。 +- 分配:bump nursery(V8)或 size-class(CPython)vs 你们的分代 arena。 +- **quickjs-oxide**:arena + `Cell` strong + 全局 `RefCell` + generation 校验,是 Rust 安全的产物,也是常数因子的主要来源。方向是 bump + 侵入式(需 `unsafe`),或至少去掉全局借用/校验。 + +### 派发 +- 栈式:QuickJS、CPython、Boa、**quickjs-oxide**。 +- 寄存器式:Lua 5.4、V8 Ignition——指令更少、栈流量更少。 +- 栈式优化:**stack caching**(Ertl:栈顶若干槽放寄存器)。 +- **quickjs-oxide**:`run` 自耗时约 **36%**,派发是最大单点;寄存器式或 stack caching 是主要杠杆。 + +### IC / 自适应特化 +- V8/JSC/SpiderMonkey:feedback vector + 多态/megamorphic IC。 +- CPython:**PEP 659 specializing adaptive interpreter**(quickening)。 +- QuickJS / Lua:基本没有。 +- **quickjs-oxide**:mono/poly-2 IC + `fusion`(superinstruction) + 编译期常量折叠 + resident 手写快分支;**没有 type feedback、专用 opcode、deopt**,因此**没有 quickening**。 +- 启示:这是**纯安全 Rust 可做**、且 QuickJS 没有的少数优势点,优先级应高。 + +### JIT +- 有:V8、JSC、SpiderMonkey、LuaJIT。无:QuickJS、Lua、CPython(默认)。 +- 本项目的 JIT 被排除,因此**上限是「极致解释器」**:可追平/略超 QuickJS,但拿不到 V8/LuaJIT 那种数量级。 + +## 3. 结论:2× 目标(无 JIT)需要什么 + +按杠杆排序: + +1. **值表示 8B + 侵入式 RC**(需受审计 `unsafe`)—— 追平 QuickJS 的入场券。 +2. **quickening + 更深 IC**(纯安全 Rust,QuickJS 没有)—— 确定的优势点。 +3. **派发改造**:stack caching 先行,评估后再决定是否寄存器式 VM;配 superinstruction。 +4. **分配器**:bump + 内联属性 + 去 arena/`RefCell`/`Result` 间接。 +5. **JIT 排除**:2× 是极限目标;且安全 Rust 相对 C 仍有税,需靠 2/3/4 补回。 + +这份比较取代原 S3 计划。S3 已据此定稿为 **`docs/reports/performance-architecture.md`**: +「8B 值表示 + quickening + 数据导向堆」的组合,而不是单纯的 16B 瘦身。 +相对上面第 1 条有一处关键修正:8B 值表示**不需要 unsafe**——本项目句柄 +本就是 arena 索引(`ObjectId{index,generation}`),把 u32 索引装进 NaN +payload 是纯位运算,「索引 NaN-box」在安全 Rust 内成立;受审计 unsafe +降级为保留席位(performance-architecture.md 方案 F)。 diff --git a/docs/reports/s3-a-plan.md b/docs/reports/s3-a-plan.md new file mode 100644 index 00000000..6599eb1c --- /dev/null +++ b/docs/reports/s3-a-plan.md @@ -0,0 +1,274 @@ +# S3-A 计划:8B 值表示——融合实施 + +> 状态:待实施。起点为 pre-A 代码基线,无前置实现资产。本文档是 +> `performance-architecture.md` §4(方案 A)的实施计划与验收规则;若两处 +> 表述冲突,**以本文档为准**。约束与证据附录继承 +> `performance-architecture.md` §0/§11/附录。 + +--- + +## 0. 设计决定(动工前钉死) + +### D1:内部值类型与转换层先行——`Value` 只是公共 API + +**事实**:`Value` 由 `src/engine/api/mod.rs:28` 公开导出(`pub use +crate::engine::value::{JsString, JsStringError, Value}`),内部数百个文件 +直接使用它。方案 A 要求内部 8B,必须把「公共值」与「内部值」拆开,否则 +改动面失控。 + +**决定**: + +1. 公共 `Value`(`src/engine/value/mod.rs:13-24`,含 `ObjectRef` 等带 + `Rc` 的 root 类型)**保持不动**,只在 `engine::api` 与宿主回调 + 适配层出现;公共签名一个不改。 +2. 新增 crate 内部类型 **`JsValue`**(`src/engine/value/js_value.rs`): + - W1–W5 为 **16B 句柄 enum**:标量内联(`Int(i32)`/`Float(f64)` 等), + 堆类型为 `{index: u32, generation: u32}` 句柄; + - A4 阶段再编码为 **u64 索引 NaN-box**(`performance-architecture.md` + §4.1),若实测不划算则停在 16B(§4.3 退路,已比现状 32B 小一半)。 + - 不实现 `Copy`/`Drop`;显式 `dup`/`release` 纪律见 §1.2。 +3. 转换层仅两个方向、只挂在 `Runtime` 上: + - `unroot`(进引擎):`&Value → JsValue`(dup 堆边)与 + `into_jsvalue(Value) → JsValue`(消费 root,省一次 retain/release 对); + - `root`(出引擎):`JsValue → Value`(retain + 包装 Rc root)。 +4. **边界规则**:`Value` 不得出现在 `engine::api` 与转换层之外——评审 + 规则,若便宜则加进 `scripts/checks/check-source-layout.py`。 +5. **穿越点清单**(公共 `Value` 进出引擎的全部位置,转换层只挂这些点): + - `Context::eval` / `eval_bytes` → `Value`(`api/context/script.rs`); + - `Context::execute` → `Value`(`api/context/calls.rs`); + - `Context::take_exception` → `Option`(`api/context/mod.rs`); + - `Context::new_array_from_values(Vec)`(`api/context/objects.rs`); + - native 调用参数缓冲:`&[Value]` / `Vec`(`builtins/dispatch.rs` + 等),内部以 `RawValue` 持有、边界再 root; + - promise jobs / module loader / test262 agent 均经 `engine::api` 或内部 + `RawValue`,没有额外的公共 `Value` 签名; + - `adapters/native` 仅转导出 `engine::api`,`adapters/web` 只用 wasm 侧 + `wasm_bindgen::JsValue`,不直接持有引擎 `Value`。 + +### D2:String/BigInt 存储层句柄化,公共 `JsString` 不动 + +**事实**:`RawValue`(`src/engine/heap/identity.rs:200-224`)对 +Object/Symbol 已是句柄(`ObjectId`/`Atom`),但 `String(JsString)` / +`BigInt(JsBigInt)` 仍是 `Rc` 负载(`value/primitive.rs:20`、 +`value/bigint.rs:104-115`)。`performance-architecture.md` §6 的 typed +arena 清单只列了 Object/VarRef/Shape/Context/FunctionBytecode——不堆化 +String/BigInt,8B 无从谈起。 + +**公共 `JsString` 保持 runtime-free 纯计算类型**,约束证据: +`from_static` 全仓 1377 处、`try_from_utf*` 853 处均为无 runtime 的纯 +构造;`impl JsString` 约 183 个方法全部 runtime-free; +`value/collection_key.rs` 是显式「no heap access」纯模块;任何带 runtime +的公共字符串表示都会破坏尺寸目标与公共表面。句柄化只发生在**值存储层**。 + +**决定**: + +1. `HeapNodeKind`(`src/engine/heap/identity.rs:127-133`)新增 **`String` + 与 `BigInt`** 两个独立 kind(不合并),与 §6 typed arena 对齐;新增 + `StringId`/`BigIntId` 句柄(`heap/identity.rs`, + `{index: u32, generation: u32}`)。**arena 节点持有现成的 + `JsString`/`JsBigInt`**(Rc 负载原样);对象槽、常量池等对 + String/BigInt 的边纳入既有事务化 retain/release 与 `Edges` 遍历。 +2. **cycle 处理:cascade-only,永不做 anchor**——string 节点零堆出边 + (rope 子节点留在 `Rc` 树内,不进 arena),BigInt 无出边,平凡成立。 +3. `StringRepr`/rope 算法与 `impl JsString` 整体不动;arena 节点只是 + `Rc` 的一个持有者。同一节点多次读出克隆同一个内部 `Rc`,`ptr_eq` + 身份快路天然保留,`same_representation` 语义不变。 +4. **公共 `JsString`/`Value` API 零改动**(`Rc`,runtime-free + 构造全保留),`adapters`/oracle 不受影响; + `RawValue::String(StringId)`/`BigInt(BigIntId)`,`JsValue` 同;堆→值 + 边界经 arena 解引用后克隆 `Rc`。 +5. `AtomTable` 基本不动:`strings` 仍按 `JsString` 键、`released_strings` / + `WeakJsString` 保持;§6.4 的 hash 缓存(`StringRepr` 头部缓存 hash、 + atom 表换 FxHash)作为独立项照做。 +6. **内容相等适配**:`RawValue` 的 `PartialEq` derive + (`identity.rs:200`)必须移除——句柄 id 相等 ≠ 内容相等。 + `collection_key.rs`(`same_value_zero`/`hash`)、StrictEq、switch + 字符串匹配改为「id 相等快路 + arena 解引用内容兜底」;动工先盘点 + 全部 `RawValue` 相等性使用点。 +7. BigInt:`RawValue::BigInt(BigIntId)` 全 arena(`Short` 也进 arena,其 + 分配成本列为测量点);**A4 开放决定**——NaN-box 下 short 收缩为 + **±2⁴⁷ 内联**(48-bit payload + kind tag,超出晋升堆句柄,语义透明), + 默认取前者,A4 开工时按测量复核。 +8. 内存语义注意:字符串/BigInt 从「`Rc` 独立分配」变为「arena 节点 + + free-list 复用 + generation」——teardown 的 `live == 0` 断言与 + `GcStats`/`HeapCounts` 公共诊断(`api/mod.rs:15` 导出)口径需同步 + 更新。 +9. **测量点**:瞬态字符串(concat/slice/`number_to_string`)的 arena + churn 会推高 zero_queue 水位,而 zero_queue 非空使 IC 快路 decline + (`ordinary_storage/ic.rs:27-35`);测量必须含 string-heavy 负载, + 确认不放倒 IC 快路。 + +### D3:`Atom` 内部 `u32`,品牌只留边界 + +**事实**:`Atom { raw: u32, generation: u32, table_id: u64 }` +(`src/engine/atom/mod.rs:52-57`)16B,相等比较逐 16B;shape 线性扫描 +(≤8 项)与迁移表键全在吃这个体积。 + +**决定**: + +1. 新增内部类型 **`AtomIdx(u32)`** newtype;保留 immediate-int 高位 tag + (`ATOM_TAG_INT`,QuickJS parity 不动)。16B branded `Atom` 只留公共 + API(`PropertyKey` 等)与跨 runtime 进入点。 +2. **存活不变量**(与 `live_node_fast` 同一论证):内部 `AtomIdx` 只能由 + 「已 retain 该 atom 的 owner」持有——shape entry、字节码 + `property_key_atoms`、pinned 集。可信路径免品牌校验(debug 构建全量 + 校验),边界全量。 +3. `AtomTable::Entry.ref_count` 改 `Cell`,retain/release 在共享借用 + 下完成——`Symbol` 从所有快路 decline 名单移除。 +4. 级联:`ShapeEntry`(`object/shape.rs:73-77`)24B→~8B(u32 atom + + flags);shape 迁移表键、shape fingerprint 同减; + `RawValue::Symbol/Private` 负载 16B→4B,为 `RawValue` 8B 化扫清最后 + 一个超标变体。 +5. `Atom` 的 `Hash` 现为 `generation<<32|raw`(`atom/mod.rs:59-65`); + 内部 `AtomIdx` 直接以 raw 作 hash(Fx),不再移位拼装。 + +--- + +## 1. 终态设计 + +### 1.1 类型格局 + +- **`JsValue`**(crate 内部,16B enum):标量内联 + + `String(StringId)`/`BigInt(BigIntId)`/`Symbol(AtomIdx)`/`Object(ObjectId)`; + 无 `Copy`/`Drop`。 +- **`RawValue`**(堆存储形态):同一套句柄 + `Private`/哨兵;与 `JsValue` + 互转是无分配的同 id 拷贝。 +- **`Value`**(公共):只在 `engine::api` 边界与宿主回调适配层出现(D1)。 + +### 1.2 所有权纪律(一条规则) + +每个存储位置(堆槽、帧、操作数栈、记录、常量池、pending_exception)持有 +其句柄的一条边: + +- **store**:拷贝入库 → retain;**move 入库 → 交接生产者边,不产生计数对**; +- **读出**:dup(retain)交出 owned `JsValue`;纯读取可原地借用; +- **overwrite / pop / finalize**:release; +- **String/BigInt 节点只在真创建点分配**:字符串/大整数产生运算、字面量 + publish、api/host 输入转换;**store 永不分配**; +- dup/release 走既有快路纪律(可信 `Cell` retain;release 经 + `release_or_defer`); +- **无 RAII 包装**:`JsValue`/`RawValue` 均无 `Drop`,所有权全靠上述显式 + 纪律——任何「自动释放」包装都会把堆访问需求带进值类型的 drop 路径, + 与「值的 drop 不需要堆」的既有纪律冲突。 + +### 1.3 owner 记录的 Drop 例外(suspension/边界层) + +挂起/恢复/放弃类记录(构造器 resume state、proxy 请求、`ReturnOwner` +等)持 `runtime: Runtime` 字段并实现 `Drop`,对仍被持有的 `JsValue` 边 +调 `release_jsvalue`——与 `ObjectRef` 先例同构(`object/mod.rs:107-111`: +owner 容器带运行时释放,不是值类型带 Drop)。约束: + +1. **适用范围**:只给 suspension/边界层 owner 记录;堆节点内的值存储 + (对象槽、常量池)仍由 finalize/overwrite 纪律负责,不走此路。 +2. **Drop 必须 nothrow**:只调 `release_jsvalue`(内部走 + `release_or_defer`,借用被持有时进 deferred 队列);禁止在 Drop 里 + 直接 `borrow_mut().unwrap()`;不跑 JS、不产生 JS 可观察行为,纯边 + 释放。 +3. **不双释放由构造保证**:消费一律经 `Option::take`(Drop 看到 `None` + 即跳过);`Vec` 字段在 Drop 里 drain 逐个 release。 +4. **成本口径**:此 `Rc` 是按控制流事件(每次挂起/放弃一次)付的边界 + 所有权,且替代等量的存量成本(原 `Value` 记录内含 `Rc` 根,持 N 个 + 值则 N 个 `Rc`,现为记录级 1 个)——不属于本阶段消灭的「按值流动 + 按次征收的 `Rc` 税」。若未来 profile 点名挂起记录变热,可对特定记录 + 类型降级为显式 release 穿线,属测量门禁后的微优化,默认不取。 + +## 2. 借用与分配放置规则 + +1. **转换提出借用区**:值→`RawValue` 的转换必须发生在任何 `state` 借用 + 之外。值刚从持有借用的结构读出的路径,先结束借用、转换、再重新借用 + ——单线程引擎、两次借用之间无 JS/native 回调,拆分借用语义不可见。 +2. **物化沉进事务**:批量/事务性存储路径(`retain_edges_transactionally`、 + publish、dense 写)把 String/BigInt 节点分配放在事务内部(本来就持 + `&mut`、本来就走边),同 shape 分配先例(`get_or_create_shape`/ + `append_transition` 在持有 `&mut RuntimeState` 的 store 事务内分配堆 + 节点)。 +3. **借用拓扑审计先于编码**:`RefCell` 借用次序是运行期行为,编译器抓 + 不到。每个工作流动笔前,先列出它触到的热路径调用点当前的借用持有 + 情况(谁持 `state` 借用、转换/分配放在哪一层),按规则 1/2 放置后 + 再写代码。重点审计:`property_ic_write_scalar` 及 IC 写路径、dense + 写、bytecode publish、挂起/恢复、`raw_property_value` 全部调用点。 +4. **升级条款**:某条路径疑似无法提出借用区时,举证标准 = 两次借用之间 + 存在 JS 可观察行为;成立则对该点用规则 2。两条都走不通才允许复审 + 「独立 `RefCell` 侧 arena」方案(拆锁式治标,与 §6 typed arena「物理 + 拆分、纪律统一」方向冲突),**不允许静默采用**。 + +## 3. 执行序列(compiler-driven) + +回退单位 = 整个大阶段(分支级);中间 commit 不要求可编译;WIP commit +只留本地,推送以绿为准。 + +| 工作流 | 内容 | +| --- | --- | +| **W1** | 句柄与转换层地基:`HeapNodeKind::String/BigInt` + typed arena(allocate/retain/release/finalize/counts + trusted 访问器);`StringId`/`BigIntId`/`AtomIdx` 句柄;`AtomTable::Entry.ref_count` `Cell` 化;`JsValue` 与四转换函数(unroot/dup/release/root)完整实现;deferred release 通路 | +| **W2** | 堆存储层:`RawValue` 句柄化(String/BigInt/Symbol/Private 全句柄,移除 `PartialEq` derive)+ `raw_value_edges` + 事务 retain-release + collection_key/index heap 化;`raw_property_value` 退役为纯 strip,仅供边界 | +| **W3** | VM 核心:`FrameBinding`/`SlotStore`/`run.rs` → `JsValue`;帧建立/拆除、挂起编解码、调用约定的显式 dup/release/move | +| **W4** | builtins 与 drivers 签名 `Value`→`JsValue` | +| **W5** | api 边界:`eval`/call/host 回调/promise jobs/module loader 的唯一 `Value`↔`JsValue` 转换层 | +| **W6** | 测试适配 + 全门禁 | + +A4(NaN-box 编码)为独立的测量门禁后续阶段,不在本序列内。 + +## 4. 验收规则 + +**设计一致性(评审第一顺位)**: + +1. 每个引入的类型/函数/构造必须属于 §1 终态设计;仅用于让中间态编译 + 通过的临时构造一律不接受——编译器报错要求的改动,要么按终态设计 + 改到底,要么不改。 +2. 借用与分配放置符合 §2;任何新增分配点必须能指出它属于「真创建点」 + 或「事务内部」。 +3. 公共表面不变:`tests/checked_string_construction.rs` 零改动通过; + `engine::api` 签名、`adapters/*` 零改动;任何需要改公共测试的迹象 + 即警报。 +4. `runtime`+`Drop` 只出现在 §1.3 范围内的 owner 记录上;逐迭代结构或 + 逐值容器中出现即评审驳回。 + +**尺寸断言(编译期钉死)**:`JsValue` = 16B;`AtomIdx` = 4B; +`ShapeEntry` = 8B;`RawValue` ≤ 16B。 + +**语义门禁**:`RawValue`/`JsValue` 均不 derive `PartialEq`,相等性使用点 +全部改为「id 快路 + 内容兜底」,SameValueZero 语义逐点核对 +(`collection_key`、StrictEq、switch 字符串匹配);teardown +`live == 0` 断言与 `GcStats`/`HeapCounts` 口径适配。 + +**全门禁(大阶段末一次)**:`cargo fmt --check` → clippy 1.88 +`-D warnings` → `cargo test --locked --workspace --all-targets` → +Test262 `--check`/`--focused`/`--full` 零回归(清理 `GIT_*`,只产 +current-source receipt,不改 `current.conf`)→ +`check-source-layout.py` + rust-only 门禁 → benchmark receipts +(`property_read_probe.py` + `scaling.py` + `run.py`,串行、独立输出 +目录,协议见 §5)→ profiling 计数器无漂移 → 实测结果记入本文档。 + +**委托执行交底**:任务拆分委托时,提示词必须包含 §1 终态设计、§1.2 +所有权纪律、§2 放置规则与「临时构造不接受」条款;评审先看设计一致性, +再看编译。 + +## 5. 阶段性能比较协议 + +1. **固定基线**:S3-A 开工前保存一份基线——无 PGO、无 LTO 的 release + 构建(`CARGO_PROFILE_RELEASE_LTO=off + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`),全量基准数字记入报告, + 作为所有阶段的固定分母之一; +2. **每阶段只比两次**:本阶段 vs 上一阶段、本阶段 vs 保存的基线;两侧 + 都是无 PGO、无 LTO 的同 flags 构建。**不做每阶段 PGO 重训**; +3. **例外与复核**:E 阶段测的就是构建配置本身,按 + `performance-architecture.md` §3 已有口径;每个大阶段(A/B/D)收尾时 + **建议**(非强制)做一次 LTO+PGO 双方复核——LTO 会改变内联与代码 + 布局,无-LTO 下的阶段胜率偶尔会在最终构建配置下翻转,复核只为确认 + 符号不变; +4. 跨协议对比允许用于累计/用户口径的报告,须标注双方构建协议。 + +## 6. 风险 + +- **手工 RC 纪律扩大 panic 面**(VM 接线起):debug 构建维持全量 + generation 校验 + 冻结向量兜底;trusted 访问器遇 stale 即 panic 的 + 政策不变。 +- **触及面全仓最大**:值类型是所有模块的公共依赖;融合大扫除期间允许 + 长时间不绿,回退单位是整个大阶段(分支级)。 +- **8B 索引 NaN-box 无生产先例**(附录 A.8):每次 deref 多一次 base + load + bounds check;A4 必须以测量定去留,退路(16B enum)不是失败 + 而是默认值。 +- **行为敏感点**:集合键 SameValueZero 与内容 hash(`collection_key.rs`)、 + StrictEq/switch 的字符串路径、teardown `live == 0` 断言、 + `GcStats`/`HeapCounts` 口径;`same_representation` 与 + `released_strings` 在 D2 下**不变**(公共 `JsString` 不动)。 diff --git a/scripts/benchmark/README.md b/scripts/benchmark/README.md index 85723a2e..f765e5ad 100644 --- a/scripts/benchmark/README.md +++ b/scripts/benchmark/README.md @@ -69,6 +69,43 @@ runner verifies matching receipts when present. External engines without a receipt are identified by binary hash/version output; attach their compiler and build configuration separately when publishing comparisons. +## Profile-guided optimization + +`pgo.py` builds a profile-guided CLI in three phases: an instrumented build, a +training run, and an optimized build. It needs the rustup `llvm-tools` +component for `llvm-profdata`: + +```sh +rustup component add llvm-tools +python3 scripts/benchmark/pgo.py --jobs 16 --v8-source ../js-engine-benchmark +``` + +By default every `scaling.py` case is trained at sizes 64 and 128, and the +external v8-v7 suite is added when `--v8-source` points at an +`js-engine-benchmark` checkout outside this repository. Training failures only +shrink coverage: raw profiles are kept and merged anyway. The optimized binary +is written under `--use-target` with a `qjs.build.json` receipt recording the +merged profile hash, training load and compiler flags. `--skip-training` +rebuilds from existing raw profiles. Ordinary release builds also take +`lto = "fat"` and `codegen-units = 1` from `[profile.release]`; comparisons +must use the same flags on both sides. + +Protocol for comparisons during staged performance work: + +- A fixed baseline is saved before the work starts: a release build with PGO + off and LTO off (`CARGO_PROFILE_RELEASE_LTO=off + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`); its full benchmark numbers are + recorded in the stage reports and serve as one of the fixed denominators. +- Each stage is compared twice: against the previous stage and against the + saved baseline, always with identical flags on both sides (no PGO, no LTO). + Per-stage PGO retraining is **not** required. +- Exception: stage E measures the build configuration itself and keeps its own + protocol. At the close of each major stage (A/B/D) a full-protocol check + (LTO+PGO, both sides retrained) is recommended but not mandatory: LTO + changes inlining and code layout, and can occasionally flip a no-LTO result. +- Cross-protocol comparisons are accepted for cumulative, user-facing deltas; + label the build protocol of both sides. + ## External V8 v7 suite ```sh diff --git a/scripts/benchmark/pgo.py b/scripts/benchmark/pgo.py new file mode 100644 index 00000000..ed500964 --- /dev/null +++ b/scripts/benchmark/pgo.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Build a profile-guided-optimization CLI: instrument, train, merge, optimize.""" +import argparse +import json +import os +from pathlib import Path +import subprocess + +from run import ROOT, command_output, digest, git_metadata + +PROFILE_GENERATE = "-Cprofile-generate={}" +PROFILE_USE = "-Cprofile-use={}" + + +def llvm_profdata(): + sysroot = Path(command_output(["rustc", "--print", "sysroot"])["stdout"]) + host = command_output(["rustc", "-vV"])["stdout"] + triple = next(line.split(":", 1)[1].strip() for line in host.splitlines() if line.startswith("host:")) + candidate = sysroot / "lib" / "rustlib" / triple / "bin" / "llvm-profdata" + if candidate.is_file(): + return candidate + found = command_output(["which", "llvm-profdata"])["stdout"] + if found: + return Path(found) + raise SystemExit("llvm-profdata not found; install the rustup `llvm-tools` component") + + +def build(target, jobs, rustflags=None): + command = ["cargo", "build", "--locked", "--release", "-p", "quickjs-oxide-cli", "--no-default-features", + "--target-dir", str(target.resolve()), "--jobs", str(jobs)] + env = {**os.environ, "QUICKJS_OXIDE_BUILD_COMMIT": git_metadata(ROOT)["commit"]["stdout"]} + if rustflags is not None: + env["RUSTFLAGS"] = rustflags + subprocess.run(command, cwd=ROOT, env=env, check=True) + binary = target.resolve() / "release" / ("qjs.exe" if os.name == "nt" else "qjs") + return binary, command + + +def run_training(command): + """Training failures only shrink coverage; raw profiles remain usable.""" + result = subprocess.run(command, cwd=ROOT) + if result.returncode: + print(f"warning: training command exited {result.returncode}; keeping its raw profiles", flush=True) + + +def train_scaling(binary, output, cases, sizes, operations, repeat, timeout): + command = ["python3", str(ROOT / "scripts" / "benchmark" / "scaling.py"), + "--engine", f"train={binary}", "--sizes", *map(str, sizes), + "--operations", str(operations), "--repeat", str(repeat), + "--timeout", str(timeout), "--output", str(output.resolve())] + for case in cases or []: + command += ["--case", case] + run_training(command) + + +def train_v8(binary, source, output, cases, repeat, timeout): + command = ["python3", str(ROOT / "scripts" / "benchmark" / "run.py"), "--suite", "v8-v7", + "--source", str(source.resolve()), "--engine", f"train={binary}", + "--repeat", str(repeat), "--timeout", str(timeout), + "--output", str(output.resolve())] + for case in cases or []: + command += ["--case", case] + run_training(command) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--generate-target", type=Path, default=ROOT / "target/pgo-generate") + parser.add_argument("--use-target", type=Path, default=ROOT / "target/pgo-use") + parser.add_argument("--profile-dir", type=Path, default=ROOT / "target/pgo-profiles") + parser.add_argument("--v8-source", type=Path, help="external js-engine-benchmark checkout for extra training") + parser.add_argument("--case", action="append", dest="cases", help="scaling case to train; repeatable, default all") + parser.add_argument("--sizes", type=int, nargs="+", default=[64, 128]) + parser.add_argument("--operations", type=int, default=32768) + parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--timeout", type=float, default=60, help="scaling training timeout") + parser.add_argument("--v8-timeout", type=float, default=300, help="v8-v7 training timeout; instrumented binaries are slow") + parser.add_argument("--jobs", type=int, default=16) + parser.add_argument("--skip-training", action="store_true", help="reuse existing raw profiles") + args = parser.parse_args() + if args.jobs < 1 or args.operations < 1 or args.repeat < 1 or args.timeout <= 0 or args.v8_timeout <= 0: + parser.error("jobs, operations, repeat and timeouts must be positive") + if args.generate_target.resolve() == args.use_target.resolve(): + parser.error("generate and use targets must differ") + profile_dir = args.profile_dir.resolve() + if not args.skip_training: + for stale in profile_dir.rglob("*.profraw"): + stale.unlink() + profile_dir.mkdir(parents=True, exist_ok=True) + # Without a unique runtime template every process overwrites the same + # default_%m_%c.profraw, so only the last training run remains. %m and %p + # keep one raw profile per process; merge combines them afterwards. + os.environ["LLVM_PROFILE_FILE"] = str(profile_dir / "%m_%p.profraw") + + print("building instrumented CLI", flush=True) + instrumented, _ = build(args.generate_target, args.jobs, PROFILE_GENERATE.format(profile_dir)) + if not args.skip_training: + raw = profile_dir / "scaling" + train_scaling(instrumented, raw, args.cases, args.sizes, args.operations, args.repeat, args.timeout) + if args.v8_source: + train_v8(instrumented, args.v8_source, profile_dir / "v8-v7", args.cases, args.repeat, args.v8_timeout) + + raw_files = sorted(profile_dir.rglob("*.profraw")) + if not raw_files: + parser.error(f"no raw profiles under {profile_dir}; run training first") + merged = profile_dir / "merged.profdata" + tool = llvm_profdata() + subprocess.run([str(tool), "merge", "-o", str(merged), *map(str, raw_files)], cwd=ROOT, check=True) + + print("building optimized CLI", flush=True) + binary, command = build(args.use_target, args.jobs, PROFILE_USE.format(merged)) + revision = git_metadata(ROOT)["commit"]["stdout"] + manifest = {"schema": "oxide-build-v1", "mode": "pgo", "vm_configuration": "stack-vm", "features": [], + "commit": revision, "binary_sha256": digest(binary), "command": command, + "rustc": command_output(["rustc", "-vV"]), "cargo": command_output(["cargo", "-V"]), + "cargo_toml_sha256": digest(ROOT / "Cargo.toml"), "cargo_lock_sha256": digest(ROOT / "Cargo.lock"), + "environment": {key: os.environ.get(key) for key in ["RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "CARGO_BUILD_TARGET"]}, + "commit_environment": revision, + "pgo": {"profile_dir": str(profile_dir), "profraw": len(raw_files), + "profdata_sha256": digest(merged), "llvm_profdata": str(tool), + "training_sizes": args.sizes, "training_operations": args.operations, + "training_repeat": args.repeat, "training_cases": args.cases or "all", + "v8_source": str(args.v8_source) if args.v8_source else None}} + binary.with_suffix(".build.json").write_text(json.dumps(manifest, indent=2) + "\n") + print(binary) + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark/property_read_probe.py b/scripts/benchmark/property_read_probe.py new file mode 100644 index 00000000..c9ea5419 --- /dev/null +++ b/scripts/benchmark/property_read_probe.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""S0 diagnostic probe for the `a.b` data-property read path. + +Generates monomorphic data-property read workloads, measures median +whole-process wall time and ns/op per engine, and can additionally record a +perf profile with a flat symbol report. This is a diagnostic probe, not a +formal score: process startup, compilation and teardown are included. + +Output directories must not already exist, protecting prior evidence. + +```sh +python3 scripts/benchmark/property_read_probe.py \ + --engine plain=target/plain/release/qjs \ + --engine profiling=target/profile-feature/release/qjs \ + --iterations 20000000 --repeat 5 --perf \ + --output target/property-read-probe +``` +""" +import argparse +import json +import os +from pathlib import Path +import statistics +import subprocess +import sys +import time + +# case -> (source template, expected expression over `n`) +WORKLOADS = { + "prop_read_int": ( + "let o = {{ a: 1, b: 2, c: 3, d: 4 }};\n" + "let s = 0;\n" + "for (let i = 0; i < {n}; i++) {{ s += o.a; }}\n" + "console.log(s);\n", + "n", + ), + "prop_read_obj": ( + "let o = {{ a: {{ x: 1 }}, b: {{ x: 2 }} }};\n" + "let s = 0;\n" + "for (let i = 0; i < {n}; i++) {{ s += o.a.x; }}\n" + "console.log(s);\n", + "n", + ), + "prop_read_string": ( + "let o = {{ a: \"hello\", b: \"world\" }};\n" + "let s = 0;\n" + "for (let i = 0; i < {n}; i++) {{ s += o.a.length; }}\n" + "console.log(s);\n", + "5*n", + ), +} + + +def parse_engine(value): + name, _, path = value.partition("=") + if not name or not path: + raise argparse.ArgumentTypeError("--engine must be name=path") + return name, os.path.abspath(path) + + +def write_workloads(directory, iterations): + directory.mkdir(parents=True, exist_ok=True) + rows = [] + for case, (template, expected_expr) in WORKLOADS.items(): + source = template.format(n=iterations) + path = directory / f"{case}.js" + path.write_text(source) + rows.append({ + "case": case, + "path": str(path), + "iterations": iterations, + "expected": eval(expected_expr, {"n": iterations}), # noqa: S307 (fixed expression) + }) + return rows + + +def measure(engine, workload, repeat, timeout): + samples = [] + output = None + for _ in range(repeat): + started = time.perf_counter() + result = subprocess.run( + [engine, workload["path"]], + capture_output=True, + timeout=timeout, + ) + elapsed = time.perf_counter() - started + if result.returncode != 0: + return None, result.stderr.decode(errors="replace") + output = result.stdout.decode().strip() + if output != str(workload["expected"]): + return None, f"unexpected output {output!r}, expected {workload['expected']!r}" + samples.append(elapsed) + samples.sort() + return { + "samples_s": samples, + "median_s": statistics.median(samples), + "min_s": samples[0], + "max_s": samples[-1], + "ns_per_iteration": statistics.median(samples) / workload["iterations"] * 1e9, + }, None + + +def perf_profile(binary, perf_bin, workload, output, timeout): + data = output / f"perf-{workload['case']}.data" + report = output / f"perf-{workload['case']}.txt" + record = subprocess.run( + [perf_bin, "record", "-q", "-g", "-o", str(data), "--", binary, workload["path"]], + capture_output=True, + timeout=timeout, + ) + if record.returncode != 0: + return {"case": workload["case"], "error": record.stderr.decode(errors="replace")} + shown = subprocess.run( + [perf_bin, "report", "-i", str(data), "--stdio", "--no-children"], + capture_output=True, + timeout=timeout, + ) + report.write_text(shown.stdout.decode(errors="replace")) + return {"case": workload["case"], "data": str(data), "report": str(report)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--engine", action="append", type=parse_engine, required=True, + metavar="NAME=PATH", help="engine binary; repeatable") + parser.add_argument("--iterations", type=int, default=20_000_000) + parser.add_argument("--repeat", type=int, default=5) + parser.add_argument("--timeout", type=float, default=600.0) + parser.add_argument("--perf", action="store_true", help="record a perf profile per case") + parser.add_argument("--perf-engine", help="engine name to profile (default: first)") + parser.add_argument("--perf-bin", default="perf") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.iterations < 1 or args.repeat < 1: + parser.error("iterations and repeat must be positive") + + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=False) + workloads = write_workloads(output / "workloads", args.iterations) + + results = {} + samples_path = output / "samples.jsonl" + with samples_path.open("w") as samples_file: + for name, engine in args.engine: + if not os.path.isfile(engine): + parser.error(f"engine binary not found: {engine}") + for workload in workloads: + summary, error = measure(engine, workload, args.repeat, args.timeout) + record = {"engine": name, "case": workload["case"], "error": error, "summary": summary} + results[(name, workload["case"])] = record + samples_file.write(json.dumps(record) + "\n") + + perf = [] + if args.perf: + target = args.perf_engine or args.engine[0][0] + binary = dict(args.engine)[target] + for workload in workloads: + perf.append(perf_profile(binary, args.perf_bin, workload, output, args.timeout)) + + metadata = { + "schema": "oxide-property-read-s0-v1", + "iterations": args.iterations, + "repeat": args.repeat, + "engines": [{"name": name, "path": path} for name, path in args.engine], + "perf_engine": args.perf_engine if args.perf else None, + } + (output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n") + + lines = ["# S0 property-read probe", "", + f"iterations={args.iterations} repeat={args.repeat}", "", + "| case | engine | median ms | ns/op |", "| --- | --- | ---: | ---: |"] + for name, _ in args.engine: + for workload in workloads: + record = results[(name, workload["case"])] + if record["summary"] is None: + lines.append(f"| {workload['case']} | {name} | error | {record['error']} |") + else: + summary = record["summary"] + lines.append( + f"| {workload['case']} | {name} | {summary['median_s']*1000:.2f} " + f"| {summary['ns_per_iteration']:.2f} |" + ) + if perf: + lines += ["", "perf reports:"] + lines += [f"- {entry.get('report', entry.get('error'))}" for entry in perf] + (output / "report.md").write_text("\n".join(lines) + "\n") + print("\n".join(lines)) + + if any(record["error"] for record in results.values()): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/engine/api/context/mod.rs b/src/engine/api/context/mod.rs index 346c1386..0526d61f 100644 --- a/src/engine/api/context/mod.rs +++ b/src/engine/api/context/mod.rs @@ -79,9 +79,9 @@ impl Context { fn finish_completion(&mut self, completion: Completion) -> Result { match completion { - Completion::Return(value) => Ok(value), + Completion::Return(value) => self.runtime.root_and_release_jsvalue(value), Completion::Throw(value) => { - self.runtime.set_pending_exception(value)?; + self.runtime.set_pending_exception_jsvalue(value)?; Err(RuntimeError::Exception) } } diff --git a/src/engine/api/profiling.rs b/src/engine/api/profiling.rs index 7c64029f..06291913 100644 --- a/src/engine/api/profiling.rs +++ b/src/engine/api/profiling.rs @@ -18,8 +18,8 @@ pub(crate) use cost::{ }; pub(crate) use cost::{ record_call_buffer_capacity, record_call_buffer_copies, record_call_buffer_initialized, - record_call_buffer_moves, record_call_buffer_observed, record_call_buffer_share, - record_call_raw_buffer_copies, + record_call_buffer_js_value_copies, record_call_buffer_moves, record_call_buffer_observed, + record_call_buffer_share, record_call_raw_buffer_copies, }; use super::Runtime; diff --git a/src/engine/api/profiling/cost.rs b/src/engine/api/profiling/cost.rs index bf90898f..ca737cc0 100644 --- a/src/engine/api/profiling/cost.rs +++ b/src/engine/api/profiling/cost.rs @@ -11,8 +11,8 @@ mod buffers; pub use buffers::CallBufferCost; pub(crate) use buffers::{ record_call_buffer_capacity, record_call_buffer_copies, record_call_buffer_initialized, - record_call_buffer_moves, record_call_buffer_observed, record_call_buffer_share, - record_call_raw_buffer_copies, + record_call_buffer_js_value_copies, record_call_buffer_moves, record_call_buffer_observed, + record_call_buffer_share, record_call_raw_buffer_copies, }; mod phases; pub(crate) use phases::{CompilePhase, PhaseTimer}; diff --git a/src/engine/api/profiling/cost/buffers.rs b/src/engine/api/profiling/cost/buffers.rs index 4f5a8e6a..06080c9d 100644 --- a/src/engine/api/profiling/cost/buffers.rs +++ b/src/engine/api/profiling/cost/buffers.rs @@ -1,7 +1,10 @@ //! Producer-local temporary buffer diagnostics. Counts are cumulative; Value //! copies, rooted promotions and raw edges are separate, never additive totals. use super::current; -use crate::engine::{heap::RawValue, value::Value}; +use crate::engine::{ + heap::RawValue, + value::{JsValue, Value}, +}; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct CallBufferCost { @@ -130,6 +133,26 @@ pub(crate) fn record_call_buffer_copies(name: &'static str, values: &[Value]) { } } +/// Internal-handle copy accounting: object/symbol handles own one edge, string +/// and BigInt handles own one payload edge, and the rest are immediates. +pub(crate) fn record_call_buffer_js_value_copies(name: &'static str, values: &[JsValue]) { + let Some(collector) = current() else { + return; + }; + let mut costs = collector.borrow_mut(); + let cost = costs.call_buffers.entry(name).or_default(); + cost.values_copied = cost.values_copied.saturating_add(values.len() as u64); + cost.slots_initialized = cost.slots_initialized.saturating_add(values.len() as u64); + for value in values { + let counter = match value { + JsValue::Object(_) | JsValue::Symbol(_) => &mut cost.heap_root_copies, + JsValue::String(_) | JsValue::BigInt(_) => &mut cost.primitive_rc_copies, + _ => &mut cost.immediate_copies, + }; + *counter = counter.saturating_add(1); + } +} + pub(crate) fn record_call_buffer_moves(name: &'static str, count: usize) { let Some(collector) = current() else { return; @@ -155,7 +178,7 @@ pub(crate) fn record_call_raw_buffer_copies(name: &'static str, values: &[RawVal RawValue::String(_) => { cost.raw_primitive_rc_copies = cost.raw_primitive_rc_copies.saturating_add(1); } - RawValue::BigInt(value) if value.as_i64().is_none() => { + RawValue::BigInt(_) => { cost.raw_primitive_rc_copies = cost.raw_primitive_rc_copies.saturating_add(1); } _ => {} diff --git a/src/engine/api/test262_agent.rs b/src/engine/api/test262_agent.rs index 8eea7875..47ce279a 100644 --- a/src/engine/api/test262_agent.rs +++ b/src/engine/api/test262_agent.rs @@ -475,6 +475,15 @@ fn run_agent_worker( // while holding the coordinator mutex. Import and callback execution // therefore happen strictly after this worker's ACK. let delivery = session.wait_for_broadcast(sequence)?; + #[cfg(debug_assertions)] + { + let state = runtime.0.state.borrow(); + eprintln!( + "[worker-before-import] live={} roots={:?}", + state.heap.counts().live, + state.heap.debug_external_roots() + ); + } let shared = match context.import_shared_array_buffer(delivery.handle) { Ok(shared) => shared, Err(error) => { @@ -488,6 +497,15 @@ fn run_agent_worker( break; } }; + #[cfg(debug_assertions)] + { + let state = runtime.0.state.borrow(); + eprintln!( + "[worker-after-import] live={} roots={:?}", + state.heap.counts().live, + state.heap.debug_external_roots() + ); + } if let Err(error) = context.call( &callback, Value::Undefined, @@ -501,6 +519,15 @@ fn run_agent_worker( ); } + #[cfg(debug_assertions)] + { + let state = runtime.0.state.borrow(); + eprintln!( + "[worker-after-call] live={} roots={:?}", + state.heap.counts().live, + state.heap.debug_external_roots() + ); + } // Pinned QuickJS clears broadcast_func immediately after JS_Call. A // synchronous replacement is therefore discarded, while a Promise // job can install the next callback during the following drain pass. @@ -624,13 +651,19 @@ impl Runtime { step = match step { AgentStep::Complete(result) => return Ok(result), AgentStep::String { value, resume } => { + let value = self.root_and_release_jsvalue(value)?; let result = match self.native_to_js_string(realm, &value)? { - NativeConversion::Value(value) => Completion::Return(Value::String(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => { + Completion::Return(self.into_jsvalue(Value::String(value))?) + } + NativeConversion::Throw(value) => { + Completion::Throw(self.into_jsvalue(value)?) + } }; resume.resume(self, result)? } AgentStep::Number { value, resume } => { + let value = self.root_and_release_jsvalue(value)?; resume.number(self, self.native_to_number(realm, &value)?)? } }; @@ -642,7 +675,7 @@ impl Runtime { realm: ContextId, message: &str, ) -> Result { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, message, @@ -1314,4 +1347,441 @@ $262.agent.start(` ); } } + + #[test] + fn zz_probe_bigint_literal() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + context.eval("var x = 12345678901234567890n; 'ok'").unwrap(); + } + + #[test] + fn zz_probe_string_literal() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + context.eval("var x = 'abcdefghij'; 'ok'").unwrap(); + } + + #[test] + fn zz_probe_agent_install_only() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let session = Test262AgentSession::new(Runtime::new); + context.install_test262_host_with_agent(&session).unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_agent_own_keys() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let session = Test262AgentSession::new(Runtime::new); + context.install_test262_host_with_agent(&session).unwrap(); + eval_string(&mut context, "Reflect.ownKeys($262.agent).length; 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_agent_descriptor_262() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let session = Test262AgentSession::new(Runtime::new); + context.install_test262_host_with_agent(&session).unwrap(); + eval_string( + &mut context, + "Object.getOwnPropertyDescriptor($262, 'agent') && 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_agent_descriptor_method() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let session = Test262AgentSession::new(Runtime::new); + context.install_test262_host_with_agent(&session).unwrap(); + eval_string( + &mut context, + "Object.getOwnPropertyDescriptor($262.agent, 'start') && 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_agent_descriptor_name() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let session = Test262AgentSession::new(Runtime::new); + context.install_test262_host_with_agent(&session).unwrap(); + eval_string( + &mut context, + "Object.getOwnPropertyDescriptor($262.agent.start, 'name') && 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_plain_own_keys() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Reflect.ownKeys({a:1}).length + ''"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_plain_gopd() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "Object.getOwnPropertyDescriptor({a:1}, 'a') && 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_plain_keys() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Object.keys({a:1}).length + ''"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_gopd_discard() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "Object.getOwnPropertyDescriptor({a:1}, 'a'); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_gopd_missing() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "Object.getOwnPropertyDescriptor({a:1}, 'b'); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_reflect_get() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Reflect.get({a:1}, 'a'); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_has_own() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Object.hasOwn({a:1}, 'a'); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_reflect_has() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Reflect.has({a:1}, 'a'); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_reflect_get_receiver() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Reflect.get({a:1}, 'a', {}); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_object_define_property() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "Object.defineProperty({}, 'a', {value:1}); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_with_object_argument() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context + .eval("(function(sab, value){ return sab.byteLength + value; })") + .unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + context + .call( + &callable, + Value::Undefined, + &[Value::Object(shared), Value::Int(1)], + ) + .unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_object_arg_ignored() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context.eval("(function(){ return 1; })").unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + context + .call(&callable, Value::Undefined, &[Value::Object(shared)]) + .unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_object_arg_returned() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context.eval("(function(sab){ return sab; })").unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + let result = context + .call(&callable, Value::Undefined, &[Value::Object(shared)]) + .unwrap(); + drop(result); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_object_this() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context.eval("(function(){ return 1; })").unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + context.call(&callable, Value::Object(shared), &[]).unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_object_arg_native() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context.eval("Object.keys").unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + context + .call(&callable, Value::Undefined, &[Value::Object(shared)]) + .unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_object_arg_byte_length() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context + .eval("(function(sab){ return sab.byteLength; })") + .unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + context + .call(&callable, Value::Undefined, &[Value::Object(shared)]) + .unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_call_object_arg_get_prototype() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let handle = SharedBufferHandle::new(4, None).unwrap(); + let shared = context.import_shared_array_buffer(handle).unwrap(); + let function = context + .eval("(function(sab){ return Object.getPrototypeOf(sab) ? 1 : 0; })") + .unwrap(); + let callable = runtime.callable_from_value(function).unwrap(); + context + .call(&callable, Value::Undefined, &[Value::Object(shared)]) + .unwrap(); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_promise_then() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Promise.resolve().then(function () {}); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_promise_ctor_direct() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "new Promise(function (r) { r(); }); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_promise_species_undefined() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "var p = Promise.resolve(); Object.defineProperty(p, 'constructor', {value: undefined}); p.then(function () {}); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_promise_then_species_ctor() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "class P extends Promise {}; var p = new P(function (r) { r(); }); p.then(function () {}); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_class_ctor_direct() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "class P extends Promise {}; new P(function (r) { r(); }); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_class_reflect_construct() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "class P extends Promise {}; Reflect.construct(P, [function (r) { r(); }], P); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_promise_species_read_only() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "var p = Promise.resolve(); Object.defineProperty(p, 'constructor', {value: {get [Symbol.species]() { return undefined; }}}); p.then(function () {}); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_promise_resolve() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "Promise.resolve(); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_ab_construct() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string(&mut context, "var b = new ArrayBuffer(4); 'ok'"); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_ab_getter_call() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "var b = new ArrayBuffer(4); var g = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get; var n = g.call(b); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_shared_byte_length() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "var b = new SharedArrayBuffer(4); var n = b.byteLength; 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_array_buffer_byte_length() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "var b = new ArrayBuffer(4); var n = b.byteLength; 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } + + #[test] + fn zz_probe_shared_own_property_descriptor() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + eval_string( + &mut context, + "var b = new SharedArrayBuffer(4); Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, 'byteLength').get.call(b); 'ok'", + ); + drop(context); + runtime.run_gc().unwrap(); + } } diff --git a/src/engine/api/test262_agent/operation.rs b/src/engine/api/test262_agent/operation.rs index b6130f4a..fdf14198 100644 --- a/src/engine/api/test262_agent/operation.rs +++ b/src/engine/api/test262_agent/operation.rs @@ -6,7 +6,7 @@ use super::{ use crate::engine::api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::builtins::native::Test262AgentKind; use crate::engine::heap::{ContextId, shared_memory::SharedBufferHandle}; -use crate::engine::value::{JsString, Value, conversion::NativeConversion}; +use crate::engine::value::{JsString, JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -14,8 +14,8 @@ use crate::engine::vm::{ pub(crate) enum AgentStep { Complete(Completion), - String { value: Value, resume: AgentResume }, - Number { value: Value, resume: AgentResume }, + String { value: JsValue, resume: AgentResume }, + Number { value: JsValue, resume: AgentResume }, } enum Phase { Start, @@ -49,11 +49,13 @@ impl AgentStep { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(runtime); return Err(RuntimeError::Invariant( "Test262 agent function received a constructor invocation", )); }; + invocation.release(runtime)?; let Some((session, role)) = registered_session_and_role(runtime, realm) else { return Err(RuntimeError::Invariant( "Test262 agent function has no registered session", @@ -68,7 +70,7 @@ impl AgentStep { } if kind == Test262AgentKind::Start { Ok(Self::String { - value: arguments.readable[0].clone(), + value: runtime.dup_jsvalue(&arguments.readable[0])?, resume: AgentResume(Box::new(AgentResumeState { realm, session, @@ -77,16 +79,18 @@ impl AgentStep { }) } else { // Brand/detached/shared checks precede observable numeric conversion. - let handle = match runtime - .test262_agent_export_broadcast_buffer(realm, &arguments.readable[0])? - { - NativeConversion::Value(handle) => handle, - NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); - } - }; + let buffer = runtime.root_value(&arguments.readable[0])?; + let handle = + match runtime.test262_agent_export_broadcast_buffer(realm, &buffer)? { + NativeConversion::Value(handle) => handle, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::Number { - value: arguments.readable[1].clone(), + value: runtime.dup_jsvalue(&arguments.readable[1])?, resume: AgentResume(Box::new(AgentResumeState { realm, session, @@ -96,7 +100,7 @@ impl AgentStep { } } Test262AgentKind::Report => Ok(Self::String { - value: arguments.readable[0].clone(), + value: runtime.dup_jsvalue(&arguments.readable[0])?, resume: AgentResume(Box::new(AgentResumeState { realm, session, @@ -104,7 +108,7 @@ impl AgentStep { })), }), Test262AgentKind::Sleep => Ok(Self::Number { - value: arguments.readable[0].clone(), + value: runtime.dup_jsvalue(&arguments.readable[0])?, resume: AgentResume(Box::new(AgentResumeState { realm, session, @@ -114,8 +118,10 @@ impl AgentStep { Test262AgentKind::GetReport => (|| -> Result { let report = lock_unpoisoned(&session.inner.reports).pop_front(); Ok(Completion::Return(match report { - Some(report) => Value::String(JsString::try_from_utf8(&report)?), - None => Value::Null, + Some(report) => { + runtime.into_jsvalue(Value::String(JsString::try_from_utf8(&report)?))? + } + None => JsValue::Null, })) })() .map(Self::Complete), @@ -125,7 +131,7 @@ impl AgentStep { .test262_agent_type_error(realm, "must be called inside an agent"); } // Pinned QuickJS performs no state transition or signal here. - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) })() .map(Self::Complete), Test262AgentKind::ReceiveBroadcast => (|| -> Result { @@ -133,7 +139,8 @@ impl AgentStep { return runtime .test262_agent_type_error(realm, "must be called inside an agent"); } - let callback = match &arguments.readable[0] { + let argument = runtime.root_value(&arguments.readable[0])?; + let callback = match &argument { Value::Object(object) => runtime.as_callable(object)?, _ => None, }; @@ -141,13 +148,13 @@ impl AgentStep { return runtime.test262_agent_type_error(realm, "expecting function"); }; install_worker_callback(runtime.domain_id(), callback); - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) })() .map(Self::Complete), Test262AgentKind::MonotonicNow => { let milliseconds = session.inner.clock_origin.elapsed().as_millis(); #[allow(clippy::cast_precision_loss)] - Ok(Completion::Return(Value::Float(milliseconds as f64))) + Ok(Completion::Return(JsValue::Float(milliseconds as f64))) } .map(Self::Complete), } @@ -161,12 +168,14 @@ impl AgentResume { ) -> Result { let source = match completion { Completion::Throw(value) => return Ok(AgentStep::Complete(Completion::Throw(value))), - Completion::Return(Value::String(value)) => value, - _ => { - return Err(RuntimeError::Invariant( - "agent string conversion returned a non-string", - )); - } + Completion::Return(value) => match runtime.root_and_release_jsvalue(value)? { + Value::String(value) => value, + _ => { + return Err(RuntimeError::Invariant( + "agent string conversion returned a non-string", + )); + } + }, }; let state = *self.0; let realm = state.realm; @@ -178,7 +187,7 @@ impl AgentResume { { Ok(source) => source, Err(_) => { - return Ok(Completion::Throw(runtime.new_native_error( + return Ok(Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "agent source containing a lone UTF-16 surrogate is not implemented", @@ -186,18 +195,18 @@ impl AgentResume { } }; if let Err(error) = session.start_worker(source) { - return Ok(Completion::Throw(runtime.new_native_error( + return Ok(Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Internal, &error, )?)); } - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } Phase::Report => { let report = source; lock_unpoisoned(&session.inner.reports).push_back(report.to_utf8_lossy()); - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } _ => Err(RuntimeError::Invariant( "agent received an unexpected string reply", @@ -214,7 +223,9 @@ impl AgentResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(AgentStep::Complete(Completion::Throw(value))); + return Ok(AgentStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let state = *self.0; @@ -225,13 +236,13 @@ impl AgentResume { Phase::Broadcast(handle) => { let value = crate::engine::value::number::to_int32(value); if let Err(error) = session.broadcast(handle, value) { - return Ok(Completion::Throw(runtime.new_native_error( + return Ok(Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Internal, &error, )?)); } - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } Phase::Sleep => { let duration = Runtime::to_uint32_number(value); @@ -244,7 +255,7 @@ impl AgentResume { "sleep is unavailable on wasm targets", ); } - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } _ => Err(RuntimeError::Invariant( "agent received an unexpected number reply", diff --git a/src/engine/api/test262_host.rs b/src/engine/api/test262_host.rs index 9d9f92ab..e2381261 100644 --- a/src/engine/api/test262_host.rs +++ b/src/engine/api/test262_host.rs @@ -12,8 +12,8 @@ use crate::engine::builtins::native::NativeFunctionId; use crate::engine::heap::ContextId; use crate::engine::object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -179,18 +179,24 @@ impl Runtime { arguments: &NativeArguments, ) -> Result { use operation::EvalScriptStep; - let mut step = EvalScriptStep::start(realm, invocation, arguments)?; + let mut step = EvalScriptStep::start(self, realm, invocation, arguments)?; loop { step = match step { EvalScriptStep::Complete(result) => return Ok(result), EvalScriptStep::String { value, resume } => { + let value = self.root_and_release_jsvalue(value)?; let result = match self.native_to_js_string(realm, &value)? { - NativeConversion::Value(value) => Completion::Return(Value::String(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => { + Completion::Return(self.into_jsvalue(Value::String(value))?) + } + NativeConversion::Throw(value) => { + Completion::Throw(self.into_jsvalue(value)?) + } }; resume.resume(self, result)? } EvalScriptStep::Call { callable, receiver } => { + let receiver = self.root_and_release_jsvalue(receiver)?; return self.call_internal(realm, &callable, receiver, &[]); } }; @@ -209,7 +215,9 @@ impl Runtime { let mut child = self.new_context(); let object_262 = child.install_test262_host()?; drop(child); - Ok(Completion::Return(Value::Object(object_262))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(object_262))?, + )) } pub(crate) fn call_test262_is_html_dda( @@ -221,7 +229,7 @@ impl Runtime { "Test262 IsHTMLDDA received a constructor invocation", )); }; - Ok(Completion::Return(Value::Null)) + Ok(Completion::Return(JsValue::Null)) } } diff --git a/src/engine/api/test262_host/operation.rs b/src/engine/api/test262_host/operation.rs index 11b95cb0..e922063e 100644 --- a/src/engine/api/test262_host/operation.rs +++ b/src/engine/api/test262_host/operation.rs @@ -5,7 +5,7 @@ use crate::engine::api::{ }; use crate::engine::heap::ContextId; use crate::engine::object::CallableRef; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -14,12 +14,12 @@ use crate::engine::vm::{ pub(crate) enum EvalScriptStep { Complete(Completion), String { - value: Value, + value: JsValue, resume: EvalScriptResume, }, Call { callable: CallableRef, - receiver: Value, + receiver: JsValue, }, } pub(crate) struct EvalScriptResume { @@ -27,20 +27,23 @@ pub(crate) struct EvalScriptResume { } impl EvalScriptStep { pub(crate) fn start( + runtime: &Runtime, realm: ContextId, invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(runtime); return Err(RuntimeError::Invariant( "Test262 evalScript received a constructor invocation", )); }; + invocation.release(runtime)?; let source = arguments.readable.first().ok_or(RuntimeError::Invariant( "Test262 evalScript argument was not padded", ))?; Ok(Self::String { - value: source.clone(), + value: runtime.dup_jsvalue(source)?, resume: EvalScriptResume { realm }, }) } @@ -55,41 +58,46 @@ impl EvalScriptResume { Completion::Throw(value) => { return Ok(EvalScriptStep::Complete(Completion::Throw(value))); } - Completion::Return(Value::String(source)) => source, - _ => { - return Err(RuntimeError::Invariant( - "evalScript conversion returned a non-string", - )); - } + Completion::Return(value) => match runtime.root_and_release_jsvalue(value)? { + Value::String(source) => source, + _ => { + return Err(RuntimeError::Invariant( + "evalScript conversion returned a non-string", + )); + } + }, }; let realm = self.realm; // The compiler currently accepts UTF-8 source rather than an exact // UTF-16 code-unit stream. Reject an unpaired surrogate explicitly; // lossy replacement would silently evaluate different JavaScript. let source_units = source.utf16_units().collect::>(); - let source = - match String::from_utf16(&source_units) { - Ok(source) => source, - Err(_) => { - return Ok(EvalScriptStep::Complete(Completion::Throw(runtime.new_native_error( - realm, - NativeErrorKind::Internal, - "evalScript source containing a lone UTF-16 surrogate is not implemented", - )?))); - } - }; + let source = match String::from_utf16(&source_units) { + Ok(source) => source, + Err(_) => { + return Ok(EvalScriptStep::Complete(Completion::Throw( + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Internal, + "evalScript source containing a lone UTF-16 surrogate is not implemented", + )?, + ))); + } + }; let script = match runtime.compile_in_realm(realm, &source, EVAL_SCRIPT_FILENAME)? { Compilation::Published(script) => script, Compilation::Throw(value) => { - return Ok(EvalScriptStep::Complete(Completion::Throw(value))); + return Ok(EvalScriptStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let callable = runtime.new_bytecode_closure(realm, &script)?; let global_object = runtime.global_object_for_realm(realm)?; Ok(EvalScriptStep::Call { callable, - receiver: Value::Object(global_object), + receiver: JsValue::Object(global_object.into_handle()), }) } } diff --git a/src/engine/atom/mod.rs b/src/engine/atom/mod.rs index cabb2976..ce0c11c0 100644 --- a/src/engine/atom/mod.rs +++ b/src/engine/atom/mod.rs @@ -19,6 +19,7 @@ //! for future C-ABI compatibility without letting a stale or cross-runtime //! [`Atom`] alias the new occupant. +use std::cell::Cell; use std::collections::HashMap; use std::error::Error; use std::fmt; @@ -146,6 +147,86 @@ impl fmt::Debug for Atom { } } +/// Crate-internal unbranded atom index for trusted owners. +/// +/// `AtomIdx` carries only the compact `u32` encoding of an [`Atom`] (table +/// slot, or the immediate-integer tag space). It is held by owners which +/// already retain the atom — shape entries, bytecode key tables, pinned sets, +/// and internal value handles — under the same liveness argument as the +/// heap's trusted fast accessors: while an owning edge exists, the slot +/// cannot be reclaimed or reused. Brand checks (generation/table ID) run in +/// full at boundary conversions ([`AtomTable::brand`] / +/// [`AtomTable::unbrand`]) and in debug builds on trusted paths. +/// +/// Copying an `AtomIdx` does *not* retain the atom; explicit retain/release +/// discipline applies exactly as for [`Atom`]. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AtomIdx(u32); + +impl AtomIdx { + /// Sentinel used for "no atom", mirroring [`Atom::NULL`]. + pub const NULL: Self = Self(0); + + /// Reconstruct an index from its raw representation without validation. + /// Immediate integers remain usable; table-backed values are only safe + /// from owners which already retain the atom. + #[must_use] + pub const fn from_raw(raw: u32) -> Self { + Self(raw) + } + + /// Return the compact raw representation. + #[must_use] + pub const fn raw(self) -> u32 { + self.0 + } + + /// Whether this is the reserved null sentinel. + #[must_use] + pub const fn is_null(self) -> bool { + self.0 == 0 + } + + /// Whether this index directly encodes a non-negative integer property. + #[must_use] + pub const fn is_immediate_integer(self) -> bool { + self.0 & ATOM_TAG_INT != 0 + } + + /// Decode an immediate integer index. + #[must_use] + pub const fn immediate_integer(self) -> Option { + if self.is_immediate_integer() { + Some(self.0 & !ATOM_TAG_INT) + } else { + None + } + } + + /// Construct an immediate integer index when `value` is within + /// `QuickJS`'s direct-encoding range. + #[must_use] + pub const fn from_immediate_integer(value: u32) -> Option { + if value <= ATOM_MAX_INT { + Some(Self(ATOM_TAG_INT | value)) + } else { + None + } + } +} + +impl fmt::Debug for AtomIdx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_null() { + f.write_str("AtomIdx::NULL") + } else if let Some(value) = self.immediate_integer() { + write!(f, "AtomIdx::Integer({value})") + } else { + write!(f, "AtomIdx({})", self.0) + } + } +} + /// Internal atom classification needed to implement ECMAScript property keys. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AtomKind { @@ -254,7 +335,7 @@ impl From for AtomError { struct Entry { kind: AtomKind, text: Option, - ref_count: u32, + ref_count: Cell, pinned: bool, } @@ -266,7 +347,7 @@ impl Entry { Some(text) => AtomSpelling::Text(text), None => AtomSpelling::NoDescription, }, - ref_count: (!self.pinned).then_some(self.ref_count), + ref_count: (!self.pinned).then_some(self.ref_count.get()), is_permanent: self.pinned, } } @@ -679,19 +760,60 @@ impl AtomTable { /// Returns [`AtomError::UnknownAtom`] for an invalid or released table ID, /// or [`AtomError::RefCountOverflow`] if its counter is already maximal. pub fn retain(&mut self, atom: Atom) -> Result { + self.retain_shared(atom) + } + + /// Shared-borrow variant of [`AtomTable::retain`]. + /// + /// The counter lives in a [`Cell`], so a trusted owner can duplicate its + /// reference while the table is only immutably borrowed (symbol fast + /// paths). Brand validation runs here, at the boundary; the counter + /// update itself goes through the unbranded index operation. + /// + /// # Errors + /// + /// Returns the same errors as [`AtomTable::retain`]. + pub(crate) fn retain_shared(&self, atom: Atom) -> Result { if atom.is_null() || atom.is_immediate_integer() { return Ok(atom); } + self.entry(atom)?; + self.retain_index_shared(AtomIdx::from_raw(atom.raw()))?; + Ok(atom) + } - let entry = self.entry_mut(atom)?; + /// Duplicate one owning reference by unbranded index. + /// + /// This is the internal operation for owners which already hold their + /// atom under the retain invariant (value slots, shapes, bytecode). The + /// slot itself is validated; no brand stamp is required or checked. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid or released index, + /// or [`AtomError::RefCountOverflow`] if its counter is already maximal. + pub(crate) fn retain_index_shared(&self, index: AtomIdx) -> Result<(), AtomError> { + if index.is_null() || index.is_immediate_integer() { + return Ok(()); + } + let entry = self.entry_by_raw(index.raw())?; if entry.pinned { - return Ok(atom); + return Ok(()); } - entry.ref_count = entry - .ref_count - .checked_add(1) - .ok_or(AtomError::RefCountOverflow(atom))?; - Ok(atom) + let atom = Atom::from_raw(index.raw()); + entry.ref_count.set( + entry + .ref_count + .get() + .checked_add(1) + .ok_or(AtomError::RefCountOverflow(atom))?, + ); + Ok(()) + } + + /// Mutable-borrow form of [`AtomTable::retain_index_shared`]. + pub(crate) fn retain_index(&mut self, index: AtomIdx) -> Result<(), AtomError> { + self.retain_index_shared(index) } /// Drop one explicit owning reference. @@ -701,29 +823,156 @@ impl AtomTable { /// Returns [`AtomError::UnknownAtom`] for an invalid or already released /// table ID. pub fn release(&mut self, atom: Atom) -> Result { + if !self.release_shared(atom)? { + return Ok(if atom.is_null() || atom.is_immediate_integer() { + ReleaseOutcome::Permanent + } else { + // The entry is either pinned or still referenced; both read + // cleanly through the shared path. + let entry = self.entry(atom)?; + if entry.pinned { + ReleaseOutcome::Permanent + } else { + ReleaseOutcome::Retained(entry.ref_count.get()) + } + }); + } + self.remove_released(atom)?; + Ok(ReleaseOutcome::Removed) + } + + /// Shared-borrow release: decrement the counter without a `&mut` table. + /// + /// Returns `Ok(true)` when the counter reached zero; the caller must then + /// ensure [`AtomTable::remove_released`] runs (immediately with a mutable + /// borrow, or deferred through the runtime's deferred queue). A racing + /// retain may resurrect the entry before removal, in which case removal + /// is skipped and the new owner stands. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid, released, or + /// already-zero table ID. + pub(crate) fn release_shared(&self, atom: Atom) -> Result { if atom.is_null() || atom.is_immediate_integer() { - return Ok(ReleaseOutcome::Permanent); + return Ok(false); } + self.entry(atom)?; + self.release_index_shared(AtomIdx::from_raw(atom.raw())) + } - let index = self.valid_index(atom)?; - let entry = self.entries[index] - .as_mut() - .ok_or(AtomError::UnknownAtom(atom))?; + /// Shared-borrow release by unbranded index. + /// + /// Returns `Ok(true)` when the counter reached zero; the caller must then + /// ensure [`AtomTable::remove_released_index`] runs. See + /// [`AtomTable::release_shared`] for the resurrection semantics. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid, released, or + /// already-zero index. + pub(crate) fn release_index_shared(&self, index: AtomIdx) -> Result { + if index.is_null() || index.is_immediate_integer() { + return Ok(false); + } + let entry = self.entry_by_raw(index.raw())?; if entry.pinned { + return Ok(false); + } + let count = entry.ref_count.get(); + if count == 0 { + return Err(AtomError::UnknownAtom(Atom::from_raw(index.raw()))); + } + let next = count - 1; + entry.ref_count.set(next); + Ok(next == 0) + } + + /// Remove the slot of an atom whose counter already reached zero. + /// + /// Callers reach this after [`AtomTable::release_shared`] reported zero. + /// If a racing retain resurrected the entry (counter nonzero) or it was + /// pinned in between, removal is skipped and ownership stands. All other + /// invalid identities are an error. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid or released table ID. + pub(crate) fn remove_released(&mut self, atom: Atom) -> Result<(), AtomError> { + if atom.is_null() || atom.is_immediate_integer() { + return Ok(()); + } + self.entry(atom)?; + self.remove_released_index(AtomIdx::from_raw(atom.raw())) + } + + /// Mutable-borrow release by unbranded index: decrement the counter and + /// reclaim the slot when it reaches zero. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid or already released + /// index. + pub(crate) fn release_index(&mut self, index: AtomIdx) -> Result { + // Immediate integers and the null sentinel are permanent and carry no + // table entry; shape entries for array-index keys push these into + // heap cleanups, exactly as the branded path always has. + if index.is_null() || index.is_immediate_integer() { return Ok(ReleaseOutcome::Permanent); } + if !self.release_index_shared(index)? { + let entry = self.entry_by_raw(index.raw())?; + return Ok(if entry.pinned { + ReleaseOutcome::Permanent + } else { + ReleaseOutcome::Retained(entry.ref_count.get()) + }); + } + self.remove_released_index(index)?; + Ok(ReleaseOutcome::Removed) + } - if entry.ref_count == 0 { - return Err(AtomError::UnknownAtom(atom)); + /// Whether an unbranded index is currently live in this table. + /// + /// Null is a sentinel rather than a live atom. All well-formed immediate + /// integers are live without table storage. + #[must_use] + pub(crate) fn is_live_index(&self, index: AtomIdx) -> bool { + if index.is_null() { + return false; } - entry.ref_count -= 1; - if entry.ref_count != 0 { - return Ok(ReleaseOutcome::Retained(entry.ref_count)); + if index.is_immediate_integer() { + return true; } + self.raw_index_live(index.raw()) + } - let entry = self.entries[index] + /// Remove the slot of an atom index whose counter already reached zero. + /// + /// See [`AtomTable::remove_released`] for the resurrection semantics; this + /// is the same operation for owners holding unbranded indices. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid or released index. + pub(crate) fn remove_released_index(&mut self, index: AtomIdx) -> Result<(), AtomError> { + if index.is_null() || index.is_immediate_integer() { + return Ok(()); + } + let raw = index.raw(); + let index_usize = raw as usize; + if self.generations.get(index_usize).is_none() || self.entries.get(index_usize).is_none() { + return Err(AtomError::UnknownAtom(Atom::from_raw(raw))); + } + let entry = self.entries[index_usize] + .as_ref() + .ok_or(AtomError::UnknownAtom(Atom::from_raw(raw)))?; + if entry.pinned || entry.ref_count.get() != 0 { + return Ok(()); + } + let entry = self.entries[index_usize] .take() - .ok_or(AtomError::UnknownAtom(atom))?; + .ok_or(AtomError::UnknownAtom(Atom::from_raw(raw)))?; match entry.kind { AtomKind::String => { if let Some(text) = entry.text { @@ -742,11 +991,11 @@ impl AtomTable { AtomKind::Symbol | AtomKind::Private => {} } self.live_table_atoms -= 1; - if let Some(generation) = self.generations[index].checked_add(1) { - self.generations[index] = generation; - self.free.push(atom.raw()); + if let Some(generation) = self.generations[index_usize].checked_add(1) { + self.generations[index_usize] = generation; + self.free.push(raw); } - Ok(ReleaseOutcome::Removed) + Ok(()) } /// Make a live table-backed atom permanent. @@ -763,7 +1012,7 @@ impl AtomTable { } let entry = self.entry_mut(atom)?; entry.pinned = true; - entry.ref_count = 0; + entry.ref_count.set(0); Ok(()) } @@ -895,6 +1144,49 @@ impl AtomTable { self.valid_index(atom).is_ok() } + /// Strip the brand from a validated atom at an internal boundary. + /// + /// Trusted owners hold the resulting [`AtomIdx`] under the retain + /// invariant; the full generation/table-ID check runs here, at the + /// boundary, for every caller. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid or released table ID. + pub(crate) fn unbrand(&self, atom: Atom) -> Result { + if atom.is_null() || atom.is_immediate_integer() { + return Ok(AtomIdx::from_raw(atom.raw())); + } + self.entry(atom)?; + Ok(AtomIdx::from_raw(atom.raw())) + } + + /// Re-attach the table's brand to an internal atom index. + /// + /// This is the checked mirror of [`AtomTable::unbrand`] and performs the + /// same full validation: a stale or foreign index is rejected here, at the + /// boundary, rather than deeper in the engine. + /// + /// # Errors + /// + /// Returns [`AtomError::UnknownAtom`] for an invalid or released index. + pub(crate) fn brand(&self, index: AtomIdx) -> Result { + if index.is_null() || index.is_immediate_integer() { + return Ok(Atom::from_raw(index.raw())); + } + let atom = Atom { + raw: index.raw(), + generation: self + .generations + .get(index.raw() as usize) + .copied() + .ok_or(AtomError::UnknownAtom(Atom::from_raw(index.raw())))?, + table_id: self.table_id, + }; + self.entry(atom)?; + Ok(atom) + } + fn allocate( &mut self, kind: AtomKind, @@ -928,7 +1220,7 @@ impl AtomTable { self.entries[index_usize] = Some(Entry { kind, text, - ref_count: u32::from(!pinned), + ref_count: Cell::new(u32::from(!pinned)), pinned, }); self.live_table_atoms += 1; @@ -959,6 +1251,27 @@ impl AtomTable { .expect("valid_index guarantees a live entry")) } + /// Validate a raw table index and return its live entry, without a brand + /// stamp. Internal owners use this under the retain invariant; boundary + /// callers must keep using [`AtomTable::entry`] with a branded [`Atom`]. + fn entry_by_raw(&self, raw: u32) -> Result<&Entry, AtomError> { + let index = raw as usize; + if self.generations.get(index).is_none() { + return Err(AtomError::UnknownAtom(Atom::from_raw(raw))); + } + match self.entries.get(index) { + Some(Some(_)) => Ok(self.entries[index] + .as_ref() + .expect("entries presence was just checked")), + _ => Err(AtomError::UnknownAtom(Atom::from_raw(raw))), + } + } + + /// Slot liveness for a raw index, without a brand stamp. + fn raw_index_live(&self, raw: u32) -> bool { + self.entry_by_raw(raw).is_ok() + } + fn entry_mut(&mut self, atom: Atom) -> Result<&mut Entry, AtomError> { let index = self.valid_index(atom)?; Ok(self.entries[index] diff --git a/src/engine/atom/runtime.rs b/src/engine/atom/runtime.rs index 835b529f..1f6a0d7a 100644 --- a/src/engine/atom/runtime.rs +++ b/src/engine/atom/runtime.rs @@ -47,6 +47,23 @@ impl Runtime { /// Allocation-free numeric subset of ToPropertyKey, including numeric -0. /// Larger, negative and fractional numbers retain the full conversion path. + pub(crate) fn immediate_numeric_property_key_jsvalue( + &self, + value: &crate::engine::value::JsValue, + ) -> Option { + let index = match value { + crate::engine::value::JsValue::Int(value) => u32::try_from(*value).ok()?, + crate::engine::value::JsValue::Float(value) + if *value >= 0.0 && *value <= u32::MAX as f64 && value.fract() == 0.0 => + { + *value as u32 + } + _ => return None, + }; + Atom::from_immediate_integer(index) + .map(|atom| PropertyKey::from_owned_atom(self.clone(), atom)) + } + pub(crate) fn immediate_numeric_property_key(&self, value: &Value) -> Option { let index = match value { Value::Int(value) => u32::try_from(*value).ok()?, diff --git a/src/engine/builtins/array.rs b/src/engine/builtins/array.rs index 42bdb8ea..57cb2536 100644 --- a/src/engine/builtins/array.rs +++ b/src/engine/builtins/array.rs @@ -3,6 +3,7 @@ use crate::engine::api::error::{Error, ErrorKind, NativeErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::{ ArrayFindKind, ArrayFlattenKind, ArrayIterationKind, ArrayIteratorKind, ArrayJoinKind, @@ -20,7 +21,7 @@ use crate::engine::object::{ PropertyKey, WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation, NativeInvokeOutcome}; use std::cmp::Ordering as ComparisonOrdering; @@ -421,12 +422,7 @@ impl Runtime { let values = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Values)?; let values = match self.get_property_in_realm(realm, array_prototype, &values)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) => { - return Err(RuntimeError::Invariant( - "Array.prototype.values was not callable during alias bootstrap", - )); - } + Completion::Return(value) => self.root_and_release_jsvalue(value)?, Completion::Throw(_) => { return Err(RuntimeError::Invariant( "Array.prototype.values initialization threw during bootstrap", @@ -434,7 +430,9 @@ impl Runtime { } }; let Value::Object(values_object) = &values else { - unreachable!("Array.prototype.values bootstrap validated an object value") + return Err(RuntimeError::Invariant( + "Array.prototype.values was not callable during alias bootstrap", + )); }; self.0 .state @@ -487,7 +485,7 @@ impl Runtime { let (prototype, mut entries, mut slots) = { let object = state.heap.object(object_id)?; let shape = state.heap.shape(object.shape)?; - if shape.find(key.atom()).is_some() { + if shape.find(AtomIdx::from_raw(key.atom().raw())).is_some() { return Err(RuntimeError::Invariant( "Array unscopables autoinit property already exists", )); @@ -499,7 +497,7 @@ impl Runtime { ) }; entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: PropertyFlags::data(false, false, true), }); slots.push(PropertySlot::AutoInit(AutoInitProperty::ArrayUnscopables { @@ -558,11 +556,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - constructor::finish( - self, - realm, - constructor::ConstructorStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + constructor::finish( + self, + realm, + constructor::ConstructorStep::start(self, realm, invocation, arguments)?, + ) + }) } fn array_constructor_length( @@ -696,13 +696,18 @@ impl Runtime { )); }; let result = match arguments.readable.first() { - Some(value) => match self.internal_is_array(realm, value)? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), - }, + Some(value) => { + let value = self.root_value(value)?; + match self.internal_is_array(realm, &value)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } + } + } None => false, }; - Ok(Completion::Return(Value::Bool(result))) + Ok(Completion::Return(JsValue::Bool(result))) } pub(crate) fn call_array_species_getter( @@ -723,11 +728,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - build::finish( - self, - realm, - build::BuildStep::start(self, realm, build::BuildKind::From, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + build::finish( + self, + realm, + build::BuildStep::start( + self, + realm, + build::BuildKind::From, + invocation, + arguments, + )?, + ) + }) } fn new_array_with_length( @@ -739,7 +752,9 @@ impl Runtime { if let Some(length) = length { let length = match self.array_constructor_length(realm, &length)? { ArrayLengthConversion::Length(length) => length, - ArrayLengthConversion::Throw(value) => return Ok(Completion::Throw(value)), + ArrayLengthConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?; match self.define_own_property_in_realm( @@ -757,10 +772,12 @@ impl Runtime { "fresh Array.from result rejected its length", )); } - PropertyDefineOutcome::Throw(value) => return Ok(Completion::Throw(value)), + PropertyDefineOutcome::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } } - Ok(Completion::Return(Value::Object(array))) + Ok(Completion::Return(self.into_jsvalue(Value::Object(array))?)) } pub(crate) fn call_array_of( @@ -769,11 +786,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - build::finish( - self, - realm, - build::BuildStep::start(self, realm, build::BuildKind::Of, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + build::finish( + self, + realm, + build::BuildStep::start(self, realm, build::BuildKind::Of, invocation, arguments)?, + ) + }) } pub(crate) fn call_array_prototype_at( @@ -782,17 +801,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - indexed::finish( - self, - realm, - indexed::IndexedStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + indexed::finish( self, realm, - indexed::IndexedKind::At, - &invocation, - arguments, - )?, - ) + indexed::IndexedStep::start( + self, + realm, + indexed::IndexedKind::At, + invocation, + arguments, + )?, + ) + }) } fn native_allocate_fast_array_values( @@ -825,17 +846,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - indexed::finish( - self, - realm, - indexed::IndexedStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + indexed::finish( self, realm, - indexed::IndexedKind::With, - &invocation, - arguments, - )?, - ) + indexed::IndexedStep::start( + self, + realm, + indexed::IndexedKind::With, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_concat( @@ -844,11 +867,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - concat::finish( - self, - realm, - concat::ConcatStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + concat::finish( + self, + realm, + concat::ConcatStep::start(self, realm, invocation, arguments)?, + ) + }) } pub(crate) fn call_array_prototype_fill( @@ -857,17 +882,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - indexed::finish( - self, - realm, - indexed::IndexedStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + indexed::finish( self, realm, - indexed::IndexedKind::Fill, - &invocation, - arguments, - )?, - ) + indexed::IndexedStep::start( + self, + realm, + indexed::IndexedKind::Fill, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_iteration( @@ -877,17 +904,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - callback::finish( - self, - realm, - callback::CallbackStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + callback::finish( self, realm, - callback::CallbackKind::Iteration(kind), - &invocation, - arguments, - )?, - ) + callback::CallbackStep::start( + self, + realm, + callback::CallbackKind::Iteration(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_reduce( @@ -897,17 +926,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - callback::finish( - self, - realm, - callback::CallbackStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + callback::finish( self, realm, - callback::CallbackKind::Reduce(kind), - &invocation, - arguments, - )?, - ) + callback::CallbackStep::start( + self, + realm, + callback::CallbackKind::Reduce(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_find( @@ -917,17 +948,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - callback::finish( - self, - realm, - callback::CallbackStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + callback::finish( self, realm, - callback::CallbackKind::Find(kind), - &invocation, - arguments, - )?, - ) + callback::CallbackStep::start( + self, + realm, + callback::CallbackKind::Find(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_copy_within( @@ -936,17 +969,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - indexed::finish( - self, - realm, - indexed::IndexedStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + indexed::finish( self, realm, - indexed::IndexedKind::CopyWithin, - &invocation, - arguments, - )?, - ) + indexed::IndexedStep::start( + self, + realm, + indexed::IndexedKind::CopyWithin, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_flatten( @@ -956,11 +991,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - flatten::finish( - self, - realm, - flatten::FlattenStep::start(self, realm, kind, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + flatten::finish( + self, + realm, + flatten::FlattenStep::start(self, realm, kind, invocation, arguments)?, + ) + }) } #[allow(clippy::too_many_arguments)] @@ -999,7 +1036,9 @@ impl Runtime { .ok_or(RuntimeError::Invariant("flatten count was not numeric"))? as u64, )), - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), + Completion::Throw(value) => Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )), } } @@ -1010,17 +1049,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - indexed::finish( - self, - realm, - indexed::IndexedStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + indexed::finish( self, realm, - indexed::IndexedKind::Search(kind), - &invocation, - arguments, - )?, - ) + indexed::IndexedStep::start( + self, + realm, + indexed::IndexedKind::Search(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_join( @@ -1047,18 +1088,20 @@ impl Runtime { arguments: &NativeArguments, string_limit: usize, ) -> Result { - string::finish( - self, - realm, - string::ArrayStringStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + string::finish( self, realm, - string::ArrayStringKind::Join(kind), - &invocation, - arguments, - string_limit, - )?, - ) + string::ArrayStringStep::start( + self, + realm, + string::ArrayStringKind::Join(kind), + invocation, + arguments, + string_limit, + )?, + ) + }) } pub(crate) fn call_array_prototype_to_string( @@ -1066,22 +1109,24 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - let arguments = NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }; - string::finish( - self, - realm, - string::ArrayStringStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + let arguments = NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }; + string::finish( self, realm, - string::ArrayStringKind::ToString, - &invocation, - &arguments, - JsString::MAX_LEN, - )?, - ) + string::ArrayStringStep::start( + self, + realm, + string::ArrayStringKind::ToString, + invocation, + &arguments, + JsString::MAX_LEN, + )?, + ) + }) } pub(crate) fn call_array_prototype_pop( @@ -1090,21 +1135,23 @@ impl Runtime { kind: ArrayPopKind, invocation: NativeInvocation, ) -> Result { - let arguments = NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }; - mutation::finish( - self, - realm, - mutation::MutationStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + let arguments = NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }; + mutation::finish( self, realm, - mutation::MutationKind::Pop(kind), - &invocation, - &arguments, - )?, - ) + mutation::MutationStep::start( + self, + realm, + mutation::MutationKind::Pop(kind), + invocation, + &arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_push( @@ -1114,17 +1161,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - mutation::finish( - self, - realm, - mutation::MutationStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + mutation::finish( self, realm, - mutation::MutationKind::Push(kind), - &invocation, - arguments, - )?, - ) + mutation::MutationStep::start( + self, + realm, + mutation::MutationKind::Push(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_reverse( @@ -1132,11 +1181,13 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - reverse::finish( - self, - realm, - reverse::ReverseStep::start(self, realm, &invocation)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + reverse::finish( + self, + realm, + reverse::ReverseStep::start(self, realm, invocation)?, + ) + }) } pub(crate) fn call_array_prototype_to_reversed( @@ -1144,21 +1195,23 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - let arguments = NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }; - indexed::finish( - self, - realm, - indexed::IndexedStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + let arguments = NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }; + indexed::finish( self, realm, - indexed::IndexedKind::ToReversed, - &invocation, - &arguments, - )?, - ) + indexed::IndexedStep::start( + self, + realm, + indexed::IndexedKind::ToReversed, + invocation, + &arguments, + )?, + ) + }) } pub(in crate::engine::builtins) fn native_sort_comparator( @@ -1169,11 +1222,12 @@ impl Runtime { let argument = arguments.readable.first().ok_or(RuntimeError::Invariant( "sort comparator argv was not padded", ))?; - if matches!(argument, Value::Undefined) { + if matches!(argument, JsValue::Undefined) { return Ok(NativeConversion::Value(None)); } - if let Value::Object(object) = argument - && let Some(callable) = self.as_callable(object)? + if let JsValue::Object(id) = argument + && let Some(callable) = + self.as_callable(&ObjectRef::from_borrowed_handle(self.clone(), *id)?)? { return Ok(NativeConversion::Value(Some(callable))); } @@ -1240,11 +1294,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - sort::finish( - self, - realm, - sort::SortStep::start(self, realm, false, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + sort::finish( + self, + realm, + sort::SortStep::start(self, realm, false, invocation, arguments)?, + ) + }) } pub(crate) fn call_array_prototype_to_sorted( @@ -1253,11 +1309,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - sort::finish( - self, - realm, - sort::SortStep::start(self, realm, true, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + sort::finish( + self, + realm, + sort::SortStep::start(self, realm, true, invocation, arguments)?, + ) + }) } /// Shared Rust port of QuickJS `js_array_slice`. The upstream `splice` @@ -1271,20 +1329,22 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - slice::finish( - self, - realm, - slice::SliceStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + slice::finish( self, realm, - match kind { - ArraySliceKind::Slice => slice::SliceKind::Slice, - ArraySliceKind::Splice => slice::SliceKind::Splice, - }, - &invocation, - arguments, - )?, - ) + slice::SliceStep::start( + self, + realm, + match kind { + ArraySliceKind::Slice => slice::SliceKind::Slice, + ArraySliceKind::Splice => slice::SliceKind::Splice, + }, + invocation, + arguments, + )?, + ) + }) } /// QuickJS `js_array_toSpliced`: allocate a defining-realm dense base @@ -1296,17 +1356,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - slice::finish( - self, - realm, - slice::SliceStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + slice::finish( self, realm, - slice::SliceKind::ToSpliced, - &invocation, - arguments, - )?, - ) + slice::SliceStep::start( + self, + realm, + slice::SliceKind::ToSpliced, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_array_prototype_iterator( @@ -1321,15 +1383,19 @@ impl Runtime { )); }; let object = match this_value { - Value::Object(object) => std::borrow::Cow::Borrowed(object), - value => match self.native_to_object(realm, value.clone())? { + JsValue::Object(id) => { + std::borrow::Cow::Owned(ObjectRef::from_borrowed_handle(self.clone(), *id)?) + } + value => match self.native_to_object_jsvalue(realm, self.dup_jsvalue(value)?)? { NativeConversion::Value(object) => std::borrow::Cow::Owned(object), - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }, }; - Ok(Completion::Return(Value::Object( + Ok(Completion::Return(self.into_jsvalue(Value::Object( self.new_array_iterator(realm, &object, kind)?, - ))) + ))?)) } pub(crate) fn call_array_iterator_next( @@ -1339,9 +1405,12 @@ impl Runtime { ) -> Result { match self.call_array_iterator_next_raw(realm, invocation)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_iterator_result(realm, value, done)?, + ))?)) + } } } @@ -1350,11 +1419,15 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - super::iterator::array::finish( - self, - realm, - super::iterator::array::ArrayNextStep::start(self, realm, &invocation)?, - ) + let step = match super::iterator::array::ArrayNextStep::start(self, realm, &invocation) { + Ok(step) => step, + Err(error) => { + let _ = invocation.release(self); + return Err(error); + } + }; + invocation.release(self)?; + super::iterator::array::finish(self, realm, step) } } diff --git a/src/engine/builtins/array/build.rs b/src/engine/builtins/array/build.rs index c751b66f..9ec764f1 100644 --- a/src/engine/builtins/array/build.rs +++ b/src/engine/builtins/array/build.rs @@ -11,7 +11,7 @@ use crate::engine::{ WellKnownSymbol, operations::{InternalDefineResult, InternalSetResult}, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{ConstructorRef, NativeArguments, NativeInvocation}, @@ -123,12 +123,15 @@ impl BuildStep { let length = u32::try_from(arguments.actual_arg_count) .map_err(|_| RuntimeError::Invariant("Array.of argument count exceeded Uint32"))?; // Snapshot argv because constructor/callback requests outlive this borrow. - let values = arguments.readable[..arguments.actual_arg_count].to_vec(); + let values = arguments.readable[..arguments.actual_arg_count] + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()?; let resume = BuildResume(Box::new(BuildResumeState { pending_effect: BuildStepPending::default(), scheduler_set_key: None, realm, - constructor: this_value.clone(), + constructor: runtime.root_value(this_value)?, result: None, mapfn: None, map_this: Value::Undefined, @@ -141,21 +144,27 @@ impl BuildStep { })); return resume.construct(runtime, Some(Runtime::array_length_value(length)), false); } - let items = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant("Array.from argv was not padded"))?; + let items = runtime.root_value( + arguments + .readable + .first() + .ok_or(RuntimeError::Invariant("Array.from argv was not padded"))?, + )?; let mapfn = if arguments.actual_arg_count > 1 - && !matches!(arguments.readable[1], Value::Undefined) + && !matches!(arguments.readable[1], JsValue::Undefined) { - let callable = match &arguments.readable[1] { + let mapfn_value = runtime.root_value(&arguments.readable[1])?; + let callable = match &mapfn_value { Value::Object(object) => runtime.as_callable(object)?, _ => None, }; let Some(callable) = callable else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; Some(callable) @@ -163,7 +172,7 @@ impl BuildStep { None }; let map_this = if arguments.actual_arg_count > 2 { - arguments.readable[2].clone() + runtime.root_value(&arguments.readable[2])? } else { Value::Undefined }; @@ -174,7 +183,7 @@ impl BuildStep { "undefined" }; return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!("cannot read property 'Symbol.iterator' of {base}"), @@ -182,13 +191,13 @@ impl BuildStep { ))); } Ok(Self::request_read( - items.clone(), + runtime.into_jsvalue(items.clone())?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), BuildResume(Box::new(BuildResumeState { pending_effect: BuildStepPending::default(), scheduler_set_key: None, realm, - constructor: this_value.clone(), + constructor: runtime.root_value(this_value)?, result: None, mapfn, map_this, @@ -208,19 +217,20 @@ impl BuildResume { self.0.scheduler_set_key.take().expect("waiting Set key") } - fn abrupt(self, value: Value) -> BuildStep { + fn abrupt(self, runtime: &Runtime, value: Value) -> Result { + let completion = Completion::Throw(runtime.into_jsvalue(value)?); if matches!(self.0.phase, Phase::Map | Phase::Define) && let Mode::Iterable { iterator: Some(iterator), .. } = self.0.mode { - return BuildStep::Close { + return Ok(BuildStep::Close { iterator, - completion: Completion::Throw(value), - }; + completion, + }); } - BuildStep::Complete(Completion::Throw(value)) + Ok(BuildStep::Complete(completion)) } fn result(&self) -> Result { self.0 @@ -241,11 +251,14 @@ impl BuildResume { { let target = match runtime.constructor_from_value(self.0.realm, constructor)? { NativeConversion::Value(target) => target, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; return Ok(BuildStep::request_construct( target, - length.into_iter().collect(), + length + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, self, )); } @@ -263,7 +276,7 @@ impl BuildResume { .. } => { let callable = next.clone(); - let receiver = Value::Object(iterator.clone()); + let receiver = JsValue::Object(iterator.clone().into_handle()); self.0.phase = Phase::NextCall; // Array.from uses ordinary Call, not JS_IteratorNext2's raw // cproto fast path; parse its actual object result afterwards. @@ -275,7 +288,7 @@ impl BuildResume { )) } Mode::ArrayLike { source, length } if self.0.index < *length => { - let receiver = Value::Object(source.clone()); + let receiver = JsValue::Object(source.clone().into_handle()); self.0.phase = Phase::Value; Ok(BuildStep::request_read( receiver, @@ -306,7 +319,7 @@ impl BuildResume { Ok(BuildStep::request_set( self.result()?, runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, - Value::number(length as f64), + runtime.into_jsvalue(Value::number(length as f64))?, self, )) } @@ -315,8 +328,11 @@ impl BuildResume { self.0.phase = Phase::Map; return Ok(BuildStep::request_call( callable, - self.0.map_this.clone(), - vec![value, Value::number(self.0.index as f64)], + runtime.into_jsvalue(self.0.map_this.clone())?, + vec![ + runtime.into_jsvalue(value)?, + runtime.into_jsvalue(Value::number(self.0.index as f64))?, + ], self, )); } @@ -347,8 +363,10 @@ impl BuildResume { return Ok(BuildStep::request_parse(reply, self)); } let value = match reply { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(self.abrupt(value)), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return self.abrupt(runtime, runtime.root_and_release_jsvalue(value)?); + } }; match self.0.phase { Phase::Method => { @@ -360,7 +378,7 @@ impl BuildResume { if matches!(value, Value::Undefined | Value::Null) { let source = match runtime.native_to_object(self.0.realm, items)? { NativeConversion::Value(source) => source, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; self.0.mode = Mode::ArrayLike { source: source.clone(), @@ -368,7 +386,7 @@ impl BuildResume { }; self.0.phase = Phase::Length; return Ok(BuildStep::request_read( - Value::Object(source), + JsValue::Object(source.into_handle()), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, self, @@ -384,7 +402,7 @@ impl BuildResume { NativeErrorKind::Type, "value is not iterable", )?; - return Ok(self.abrupt(error)); + return self.abrupt(runtime, error); }; self.0.mode = Mode::Iterable { items, @@ -396,7 +414,10 @@ impl BuildResume { } Phase::Length => { self.0.phase = Phase::Number; - Ok(BuildStep::request_number(value, self)) + Ok(BuildStep::request_number( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Construct => { let Value::Object(result) = value else { @@ -406,7 +427,7 @@ impl BuildResume { }; self.0.result = Some(result); if let Mode::Iterable { items, method, .. } = &self.0.mode { - let receiver = items.clone(); + let receiver = runtime.into_jsvalue(items.clone())?; let callable = method.clone(); self.0.phase = Phase::Iterator; return Ok(BuildStep::request_call( @@ -425,7 +446,7 @@ impl BuildResume { NativeErrorKind::Type, "not an object", )?; - return Ok(self.abrupt(error)); + return self.abrupt(runtime, error); }; let Mode::Iterable { iterator: target, .. @@ -436,7 +457,7 @@ impl BuildResume { *target = Some(iterator.clone()); self.0.phase = Phase::NextMethod; Ok(BuildStep::request_read( - Value::Object(iterator), + JsValue::Object(iterator.into_handle()), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?, self, )) @@ -452,7 +473,7 @@ impl BuildResume { NativeErrorKind::Type, "not a function", )?; - return Ok(self.abrupt(error)); + return self.abrupt(runtime, error); }; let Mode::Iterable { next, .. } = &mut self.0.mode else { return Err(RuntimeError::Invariant("Array.from iterator mode missing")); @@ -474,7 +495,7 @@ impl BuildResume { ) -> Result { let number = match reply { NativeConversion::Value(number) => number, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; if !matches!(self.0.phase, Phase::Number) { return Err(RuntimeError::Invariant( @@ -501,9 +522,13 @@ impl BuildResume { )); } match reply { - ObjectIteratorStep::Throw(value) => Ok(self.abrupt(value)), + ObjectIteratorStep::Throw(value) => { + self.abrupt(runtime, runtime.root_and_release_jsvalue(value)?) + } ObjectIteratorStep::Done => self.set_length(runtime), - ObjectIteratorStep::Yield(value) => self.map(runtime, value), + ObjectIteratorStep::Yield(value) => { + self.map(runtime, runtime.root_and_release_jsvalue(value)?) + } } } pub(crate) fn defined( @@ -519,7 +544,7 @@ impl BuildResume { if let Some(value) = runtime.finish_create_indexed_data_property(self.0.realm, self.0.index, reply)? { - return Ok(self.abrupt(value)); + return self.abrupt(runtime, value); } self.0.index = self.0.index.checked_add(1).ok_or(RuntimeError::Invariant( "Array.from iterator index overflowed u64", @@ -538,11 +563,11 @@ impl BuildResume { )); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, reply)? { - return Ok(self.abrupt(value)); + return self.abrupt(runtime, value); } - Ok(BuildStep::Complete(Completion::Return(Value::Object( - self.result()?, - )))) + Ok(BuildStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.result()?))?, + ))) } } pub(crate) fn finish( @@ -554,7 +579,7 @@ pub(crate) fn finish( step = match step { BuildStep::Complete(result) => return Ok(result), BuildStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -562,13 +587,17 @@ pub(crate) fn finish( )? } BuildStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } BuildStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -576,7 +605,11 @@ pub(crate) fn finish( } BuildStep::Construct { mut resume } => { let target = resume.take_construct_target(); - let arguments = resume.take_construct_arguments(); + let arguments = resume + .take_construct_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.construct_constructor_internal(realm, &target, &target, &arguments)?, @@ -605,7 +638,7 @@ pub(crate) fn finish( BuildStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let reply = runtime.internal_set( realm, @@ -633,36 +666,40 @@ pub(crate) fn finish( #[derive(Default)] struct BuildStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, - number_value: Option, + number_value: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, construct_target: Option, - construct_arguments: Option>, + construct_arguments: Option>, parse_result: Option, define_object: Option, define_key: Option, define_descriptor: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, } impl BuildStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: BuildResume) -> Self { + pub(crate) fn request_read( + receiver: JsValue, + key: PropertyKey, + mut resume: BuildResume, + ) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: BuildResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: BuildResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: BuildResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -672,7 +709,7 @@ impl BuildStep { } pub(crate) fn request_construct( target: ConstructorRef, - arguments: Vec, + arguments: Vec, mut resume: BuildResume, ) -> Self { resume.0.pending_effect.construct_target = Some(target); @@ -697,7 +734,7 @@ impl BuildStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: BuildResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -707,7 +744,7 @@ impl BuildStep { } } impl BuildResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -721,7 +758,7 @@ impl BuildResume { .take() .expect("BuildStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -735,14 +772,14 @@ impl BuildResume { .take() .expect("BuildStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("BuildStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -756,7 +793,7 @@ impl BuildResume { .take() .expect("BuildStep Construct target") } - pub(crate) fn take_construct_arguments(&mut self) -> Vec { + pub(crate) fn take_construct_arguments(&mut self) -> Vec { self.0 .pending_effect .construct_arguments @@ -805,7 +842,7 @@ impl BuildResume { .take() .expect("BuildStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/callback.rs b/src/engine/builtins/array/callback.rs index 45fb6618..54db795a 100644 --- a/src/engine/builtins/array/callback.rs +++ b/src/engine/builtins/array/callback.rs @@ -9,7 +9,7 @@ use crate::engine::{ CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, operations::InternalDefineResult, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -63,6 +63,7 @@ impl std::ops::DerefMut for CallbackResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct CallbackResumeState { + runtime: Runtime, pending_effect: CallbackStepPending, realm: ContextId, kind: CallbackKind, @@ -79,6 +80,24 @@ pub(crate) struct CallbackResumeState { cursor: u64, selected: u64, } +impl Drop for CallbackResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.number_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} impl CallbackStep { pub(crate) fn start( runtime: &Runtime, @@ -92,27 +111,22 @@ impl CallbackStep { "Array callback requires generic invocation", )); }; - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; - let callback_value = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "Array callback argv was not padded", - ))? - .clone(); + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(object) => object, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; + let callback_value = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Array callback argv was not padded"), + )?)?; let second = if arguments.actual_arg_count > 1 { - Some( - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "Array callback second argument missing", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("Array callback second argument missing"), + )?)?) } else { None }; @@ -120,11 +134,12 @@ impl CallbackStep { object.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, CallbackResume(Box::new(CallbackResumeState { + runtime: runtime.clone(), pending_effect: CallbackStepPending::default(), realm, kind, object, - original: this_value.clone(), + original: runtime.root_value(this_value)?, callback_value, callback: None, this_arg: second.clone().unwrap_or(Value::Undefined), @@ -170,15 +185,16 @@ impl CallbackResume { Ok(CallbackStep::request_number(value, self)) } Phase::Species => { - if !matches!(value, Value::Object(_)) { + if !matches!(value, JsValue::Object(_)) { return Err(RuntimeError::Invariant( "ArraySpeciesCreate returned a primitive", )); } - self.0.result = value; + self.0.result = runtime.root_and_release_jsvalue(value)?; self.next(runtime) } Phase::Read => { + let value = runtime.root_and_release_jsvalue(value)?; if matches!(self.0.kind, CallbackKind::Reduce(_)) && self.0.accumulator.is_none() { self.0.accumulator = Some(value); self.0.cursor += 1; @@ -208,6 +224,10 @@ impl CallbackResume { }; self.0.value = value; self.0.phase = Phase::Callback; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; Ok(CallbackStep::request_call( self.0 .callback @@ -215,33 +235,39 @@ impl CallbackResume { .ok_or(RuntimeError::Invariant("Array callback missing"))? .clone(), if matches!(self.0.kind, CallbackKind::Reduce(_)) { - Value::Undefined + JsValue::Undefined } else { - self.0.this_arg.clone() + runtime.into_jsvalue(self.0.this_arg.clone())? }, arguments, self, )) } Phase::Callback => { + let value = runtime.root_and_release_jsvalue(value)?; match self.0.kind { CallbackKind::Reduce(_) => self.0.accumulator = Some(value), CallbackKind::Find(kind) => { if runtime.value_to_boolean(&value)? { - return Ok(CallbackStep::Complete(Completion::Return(match kind { - ArrayFindKind::Find | ArrayFindKind::FindLast => self.0.value, + let result = match kind { + ArrayFindKind::Find | ArrayFindKind::FindLast => { + std::mem::replace(&mut self.0.value, Value::Undefined) + } _ => Value::number(self.index() as f64), - }))); + }; + return Ok(CallbackStep::Complete(Completion::Return( + runtime.into_jsvalue(result)?, + ))); } } CallbackKind::Iteration(kind) => match kind { ArrayIterationKind::Every if !runtime.value_to_boolean(&value)? => { - return Ok(CallbackStep::Complete(Completion::Return(Value::Bool( + return Ok(CallbackStep::Complete(Completion::Return(JsValue::Bool( false, )))); } ArrayIterationKind::Some if runtime.value_to_boolean(&value)? => { - return Ok(CallbackStep::Complete(Completion::Return(Value::Bool( + return Ok(CallbackStep::Complete(Completion::Return(JsValue::Bool( true, )))); } @@ -279,7 +305,9 @@ impl CallbackResume { self.0.length = match result { NativeConversion::Value(value) => Runtime::length_from_number(value), NativeConversion::Throw(value) => { - return Ok(CallbackStep::Complete(Completion::Throw(value))); + return Ok(CallbackStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.callback = Some(runtime.callable_from_value(self.0.callback_value.clone())?); @@ -307,12 +335,14 @@ impl CallbackResume { fn next(mut self, runtime: &Runtime) -> Result { if self.0.cursor == self.0.length { let result = match self.0.kind { - CallbackKind::Iteration(_) => self.0.result, - CallbackKind::Reduce(_) => match self.0.accumulator { + CallbackKind::Iteration(_) => { + std::mem::replace(&mut self.0.result, Value::Undefined) + } + CallbackKind::Reduce(_) => match self.0.accumulator.take() { Some(value) => value, None => { return Ok(CallbackStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "empty array", @@ -325,7 +355,9 @@ impl CallbackResume { } CallbackKind::Find(_) => Value::Int(-1), }; - return Ok(CallbackStep::Complete(Completion::Return(result))); + return Ok(CallbackStep::Complete(Completion::Return( + runtime.into_jsvalue(result)?, + ))); } let key = runtime.property_key_for_index(self.index())?; if matches!(self.0.kind, CallbackKind::Find(_)) { @@ -349,7 +381,9 @@ impl CallbackResume { let present = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(CallbackStep::Complete(Completion::Throw(value))); + return Ok(CallbackStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !present { @@ -408,7 +442,9 @@ impl CallbackResume { if let Some(value) = runtime.finish_create_indexed_data_property(self.0.realm, index, result)? { - return Ok(CallbackStep::Complete(Completion::Throw(value))); + return Ok(CallbackStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } if filter { self.0.selected += 1; @@ -435,7 +471,7 @@ pub(crate) fn finish( )? } CallbackStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } CallbackStep::Has { mut resume } => { @@ -448,8 +484,12 @@ pub(crate) fn finish( } CallbackStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -500,11 +540,11 @@ mod tests { mapper_object.object_id(), ]; let invocation = NativeInvocation::Call { - this_value: Value::Object(source), + this_value: runtime.into_jsvalue(Value::Object(source)).unwrap(), }; let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![mapper], + readable: vec![runtime.into_jsvalue(mapper).unwrap()], }; let CallbackStep::Read { mut resume } = CallbackStep::start( &runtime, @@ -519,10 +559,15 @@ mod tests { let _ = resume.take_read_object(); let _ = resume.take_read_key(); - drop(invocation); - drop(arguments); + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } let CallbackStep::Number { mut resume } = resume - .resume(&runtime, Completion::Return(Value::Int(1))) + .resume(&runtime, Completion::Return(JsValue::Int(1))) .unwrap() else { panic!("expected length conversion"); @@ -539,7 +584,10 @@ mod tests { let _ = resume.take_species_length(); let CallbackStep::Has { mut resume } = resume - .resume(&runtime, Completion::Return(Value::Object(target))) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(Value::Object(target)).unwrap()), + ) .unwrap() else { panic!("expected indexed lookup"); @@ -553,8 +601,11 @@ mod tests { } drop(resume); runtime.run_gc().unwrap(); - for id in ids { - assert!(runtime.0.state.borrow().heap.object(id).is_err()); + for (i, id) in ids.into_iter().enumerate() { + assert!( + runtime.0.state.borrow().heap.object(id).is_err(), + "id index {i} still live" + ); } drop(context); drop(runtime); @@ -566,12 +617,12 @@ mod tests { struct CallbackStepPending { read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, has_object: Option, has_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, species_source: Option, species_length: Option, define_object: Option, @@ -588,7 +639,7 @@ impl CallbackStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: CallbackResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: CallbackResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -603,8 +654,8 @@ impl CallbackStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: CallbackResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -648,7 +699,7 @@ impl CallbackResume { .take() .expect("CallbackStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -676,14 +727,14 @@ impl CallbackResume { .take() .expect("CallbackStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("CallbackStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/array/concat.rs b/src/engine/builtins/array/concat.rs index 52a2fa17..5d3d8aa9 100644 --- a/src/engine/builtins/array/concat.rs +++ b/src/engine/builtins/array/concat.rs @@ -6,7 +6,7 @@ use crate::engine::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, operations::{InternalDefineResult, InternalSetResult}, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -68,16 +68,22 @@ impl ConcatStep { "Array concat requires generic invocation", )); }; - let source = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let source = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; let mut elements = Vec::with_capacity(arguments.actual_arg_count + 1); elements.push(Value::Object(source.clone())); elements.extend( arguments.readable[..arguments.actual_arg_count] .iter() - .cloned(), + .map(|value| runtime.root_value(value)) + .collect::, _>>()?, ); Ok(Self::request_species( source, @@ -121,7 +127,11 @@ impl ConcatResume { } fn too_long(&self, runtime: &Runtime) -> Result { Ok(ConcatStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Type, "Array loo long")?, + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "Array loo long", + )?, ))) } pub(crate) fn resume( @@ -130,7 +140,7 @@ impl ConcatResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(ConcatStep::Complete(Completion::Throw(value))), }; match self.0.phase { @@ -148,7 +158,9 @@ impl ConcatResume { match runtime.internal_is_array(self.0.realm, &self.0.element)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ConcatStep::Complete(Completion::Throw(value))); + return Ok(ConcatStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } else { @@ -168,7 +180,10 @@ impl ConcatResume { } Phase::Length => { self.0.phase = Phase::Number; - Ok(ConcatStep::request_number(value, self)) + Ok(ConcatStep::request_number( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Read => self.define(runtime, value, true), _ => Err(RuntimeError::Invariant("Array concat value phase mismatch")), @@ -180,7 +195,7 @@ impl ConcatResume { return Ok(ConcatStep::request_set( self.result()?, runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, - Value::number(self.0.next_index as f64), + runtime.into_jsvalue(Value::number(self.0.next_index as f64))?, self, )); }; @@ -218,7 +233,9 @@ impl ConcatResume { self.0.length = match result { NativeConversion::Value(value) => Runtime::length_from_number(value), NativeConversion::Throw(value) => { - return Ok(ConcatStep::Complete(Completion::Throw(value))); + return Ok(ConcatStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if self.0.next_index.saturating_add(self.0.length) > (1_u64 << 53) - 1 { @@ -250,7 +267,9 @@ impl ConcatResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ConcatStep::Complete(Completion::Throw(value))); + return Ok(ConcatStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if value { @@ -299,7 +318,9 @@ impl ConcatResume { if let Some(value) = runtime.finish_create_indexed_data_property(self.0.realm, self.0.next_index, result)? { - return Ok(ConcatStep::Complete(Completion::Throw(value))); + return Ok(ConcatStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.next_index += 1; if indexed { @@ -319,11 +340,13 @@ impl ConcatResume { return Err(RuntimeError::Invariant("Array concat set phase mismatch")); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(ConcatStep::Complete(Completion::Throw(value))); + return Ok(ConcatStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } - Ok(ConcatStep::Complete(Completion::Return(Value::Object( - self.result()?, - )))) + Ok(ConcatStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.result()?))?, + ))) } } pub(crate) fn finish( @@ -354,7 +377,7 @@ pub(crate) fn finish( )? } ConcatStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } ConcatStep::Has { mut resume } => { @@ -377,7 +400,7 @@ pub(crate) fn finish( ConcatStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -398,7 +421,7 @@ struct ConcatStepPending { species_source: Option, read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, has_object: Option, has_key: Option, define_object: Option, @@ -406,7 +429,7 @@ struct ConcatStepPending { define_descriptor: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, } impl ConcatStep { pub(crate) fn request_species(source: ObjectRef, mut resume: ConcatResume) -> Self { @@ -422,7 +445,7 @@ impl ConcatStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: ConcatResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: ConcatResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -449,7 +472,7 @@ impl ConcatStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: ConcatResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -480,7 +503,7 @@ impl ConcatResume { .take() .expect("ConcatStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -536,7 +559,7 @@ impl ConcatResume { .take() .expect("ConcatStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/constructor.rs b/src/engine/builtins/array/constructor.rs index e01d5426..26161026 100644 --- a/src/engine/builtins/array/constructor.rs +++ b/src/engine/builtins/array/constructor.rs @@ -6,7 +6,7 @@ use crate::engine::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, operations::{ArrayLengthConversion, InternalSetResult, PropertyDefineOutcome}, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -31,6 +31,7 @@ impl std::ops::DerefMut for ConstructorResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct ConstructorResumeState { + runtime: Runtime, pending_effect: ConstructorStepPending, scheduler_set_key: Option, realm: ContextId, @@ -39,6 +40,19 @@ pub(crate) struct ConstructorResumeState { array: Option, index: usize, } +impl Drop for ConstructorResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.set_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} impl ConstructorStep { pub(crate) fn start( runtime: &Runtime, @@ -52,19 +66,23 @@ impl ConstructorStep { )); }; let resume = ConstructorResume(Box::new(ConstructorResumeState { + runtime: runtime.clone(), pending_effect: ConstructorStepPending::default(), scheduler_set_key: None, realm, - new_target: new_target.clone(), - arguments: arguments.readable[..arguments.actual_arg_count].to_vec(), + new_target: runtime.root_value(new_target)?, + arguments: arguments.readable[..arguments.actual_arg_count] + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()?, array: None, index: 0, })); - if matches!(new_target, Value::Undefined) { - resume.resume(runtime, Completion::Return(Value::Undefined)) + if matches!(new_target, JsValue::Undefined) { + resume.resume(runtime, Completion::Return(JsValue::Undefined)) } else { Ok(Self::request_read( - new_target.clone(), + runtime.dup_jsvalue(new_target)?, runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?, resume, )) @@ -94,27 +112,31 @@ impl ConstructorResume { Completion::Throw(value) => { return Ok(ConstructorStep::Complete(Completion::Throw(value))); } - Completion::Return(Value::Object(object)) => object, - Completion::Return(_) => { - let realm = if matches!(self.0.new_target, Value::Undefined) { - self.0.realm - } else { - match runtime.function_realm_from_value(self.0.realm, &self.0.new_target)? { - NativeConversion::Value(realm) => realm, - NativeConversion::Throw(value) => { - return Ok(ConstructorStep::Complete(Completion::Throw(value))); + Completion::Return(value) => match runtime.root_and_release_jsvalue(value)? { + Value::Object(object) => object, + _ => { + let realm = if matches!(self.0.new_target, Value::Undefined) { + self.0.realm + } else { + match runtime.function_realm_from_value(self.0.realm, &self.0.new_target)? { + NativeConversion::Value(realm) => realm, + NativeConversion::Throw(value) => { + return Ok(ConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } } - } - }; - let prototype = runtime - .0 - .state - .borrow() - .heap - .context(realm)? - .array_prototype; - ObjectRef::from_borrowed_handle(runtime.clone(), prototype)? - } + }; + let prototype = runtime + .0 + .state + .borrow() + .heap + .context(realm)? + .array_prototype; + ObjectRef::from_borrowed_handle(runtime.clone(), prototype)? + } + }, }; let array = runtime.new_empty_array_with_prototype(&prototype)?; if self.0.arguments.len() == 1 @@ -124,7 +146,9 @@ impl ConstructorResume { match runtime.array_constructor_length(self.0.realm, &self.0.arguments[0])? { ArrayLengthConversion::Length(length) => length, ArrayLengthConversion::Throw(value) => { - return Ok(ConstructorStep::Complete(Completion::Throw(value))); + return Ok(ConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let key = @@ -146,11 +170,13 @@ impl ConstructorResume { )); } PropertyDefineOutcome::Throw(value) => { - return Ok(ConstructorStep::Complete(Completion::Throw(value))); + return Ok(ConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } return Ok(ConstructorStep::Complete(Completion::Return( - Value::Object(array), + runtime.into_jsvalue(Value::Object(array))?, ))); } self.0.array = Some(array); @@ -162,7 +188,7 @@ impl ConstructorResume { ))?; let Some(value) = self.0.arguments.get(self.0.index).cloned() else { return Ok(ConstructorStep::Complete(Completion::Return( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, ))); }; let index = u32::try_from(self.0.index) @@ -170,7 +196,7 @@ impl ConstructorResume { Ok(ConstructorStep::request_set( object, runtime.property_key_for_index(u64::from(index))?, - value, + runtime.into_jsvalue(value)?, self, )) } @@ -186,7 +212,9 @@ impl ConstructorResume { )); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(ConstructorStep::Complete(Completion::Throw(value))); + return Ok(ConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.index += 1; self.next(runtime) @@ -201,7 +229,7 @@ pub(crate) fn finish( step = match step { ConstructorStep::Complete(result) => return Ok(result), ConstructorStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -211,7 +239,7 @@ pub(crate) fn finish( ConstructorStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -239,22 +267,37 @@ mod tests { let argument = runtime.new_object(None).unwrap(); let ids = [target.object_id(), argument.object_id()]; let invocation = NativeInvocation::Construct { - new_target: Value::Object(target), + new_target: runtime + .unroot_value(&Value::Object(target.clone())) + .unwrap(), }; let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::Object(argument)], + readable: vec![ + runtime + .unroot_value(&Value::Object(argument.clone())) + .unwrap(), + ], }; let ConstructorStep::Read { mut resume } = ConstructorStep::start(&runtime, context.realm, &invocation, &arguments).unwrap() else { panic!("expected prototype lookup"); }; - let _ = resume.take_read_receiver(); let _ = resume.take_read_key(); + runtime + .release_jsvalue(resume.take_read_receiver()) + .unwrap(); - drop(arguments); - drop(invocation); + let NativeInvocation::Construct { new_target } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(new_target).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + drop(target); + drop(argument); runtime.run_gc().unwrap(); for id in ids { assert!(runtime.0.state.borrow().heap.object(id).is_ok()); @@ -272,15 +315,15 @@ mod tests { #[derive(Default)] struct ConstructorStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, } impl ConstructorStep { pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: ConstructorResume, ) -> Self { @@ -291,7 +334,7 @@ impl ConstructorStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: ConstructorResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -301,7 +344,7 @@ impl ConstructorStep { } } impl ConstructorResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -329,7 +372,7 @@ impl ConstructorResume { .take() .expect("ConstructorStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/copy.rs b/src/engine/builtins/array/copy.rs index 802df05f..82a93eab 100644 --- a/src/engine/builtins/array/copy.rs +++ b/src/engine/builtins/array/copy.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::Completion, }; pub(crate) enum CopyStep { @@ -100,7 +100,7 @@ impl CopyResume { } fn next(mut self, runtime: &Runtime) -> Result { if self.0.offset == self.0.count { - return Ok(CopyStep::Complete(Completion::Return(Value::Undefined))); + return Ok(CopyStep::Complete(Completion::Return(JsValue::Undefined))); } self.0.phase = Phase::Has; Ok(CopyStep::request_has( @@ -117,7 +117,9 @@ impl CopyResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(CopyStep::Complete(Completion::Throw(value))); + return Ok(CopyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -140,7 +142,7 @@ impl CopyResume { Phase::Write => { if !value { return Ok(CopyStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "could not delete property", @@ -183,7 +185,9 @@ impl CopyResume { return Err(RuntimeError::Invariant("Array copy set phase mismatch")); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(CopyStep::Complete(Completion::Throw(value))); + return Ok(CopyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.offset += 1; self.next(runtime) @@ -216,7 +220,7 @@ pub(crate) fn finish( CopyStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -248,7 +252,7 @@ struct CopyStepPending { read_key: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, delete_object: Option, delete_key: Option, } @@ -270,7 +274,7 @@ impl CopyStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: CopyResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -331,7 +335,7 @@ impl CopyResume { .take() .expect("CopyStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/flatten.rs b/src/engine/builtins/array/flatten.rs index 297e544b..7f20ee1c 100644 --- a/src/engine/builtins/array/flatten.rs +++ b/src/engine/builtins/array/flatten.rs @@ -8,7 +8,7 @@ use crate::engine::{ CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, operations::InternalDefineResult, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -81,10 +81,15 @@ impl FlattenStep { "Array flatten requires generic invocation", )); }; - let source = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let source = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::request_read( source.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, @@ -97,18 +102,16 @@ impl FlattenStep { source_index: 0, source_length: 0, depth: 1, - argument: arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined), + argument: match arguments.readable.first() { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + }, mapper: None, mapper_this: if arguments.actual_arg_count > 1 { - arguments - .readable - .get(1) - .cloned() - .unwrap_or(Value::Undefined) + match arguments.readable.get(1) { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + } } else { Value::Undefined }, @@ -166,13 +169,16 @@ impl FlattenResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(FlattenStep::Complete(Completion::Throw(value))), }; match self.0.phase { Phase::Length => { self.0.phase = Phase::LengthNumber; - Ok(FlattenStep::request_number(value, self)) + Ok(FlattenStep::request_number( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Species => { let Value::Object(target) = value else { @@ -198,11 +204,11 @@ impl FlattenResume { .as_ref() .ok_or(RuntimeError::Invariant("flatten mapper missing"))? .clone(), - self.0.mapper_this.clone(), + runtime.into_jsvalue(self.0.mapper_this.clone())?, vec![ - value, - Value::number(self.0.source_index as f64), - Value::Object(self.0.source.clone()), + runtime.into_jsvalue(value)?, + runtime.into_jsvalue(Value::number(self.0.source_index as f64))?, + runtime.into_jsvalue(Value::Object(self.0.source.clone()))?, ], self, )); @@ -212,7 +218,10 @@ impl FlattenResume { Phase::Mapper => self.visit(runtime, value), Phase::NestedLength => { self.0.phase = Phase::NestedNumber; - Ok(FlattenStep::request_number(value, self)) + Ok(FlattenStep::request_number( + runtime.into_jsvalue(value)?, + self, + )) } _ => Err(RuntimeError::Invariant( "Array flatten value phase mismatch", @@ -227,7 +236,9 @@ impl FlattenResume { let number = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(FlattenStep::Complete(Completion::Throw(value))); + return Ok(FlattenStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -237,7 +248,10 @@ impl FlattenResume { self.0.mapper = Some(runtime.callable_from_value(self.0.argument.clone())?); } else if !matches!(self.0.argument, Value::Undefined) { self.0.phase = Phase::Depth; - return Ok(FlattenStep::request_number(self.0.argument.clone(), self)); + return Ok(FlattenStep::request_number( + runtime.into_jsvalue(self.0.argument.clone())?, + self, + )); } self.species() } @@ -274,7 +288,11 @@ impl FlattenResume { } fn overflow(&self, runtime: &Runtime) -> Result { Ok(FlattenStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Internal, "stack overflow")?, + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Internal, + "stack overflow", + )?, ))) } fn begin(mut self, runtime: &Runtime) -> Result { @@ -293,16 +311,17 @@ impl FlattenResume { fn next(mut self, runtime: &Runtime) -> Result { loop { let Some(frame) = self.0.frames.last_mut() else { + let value = if self.0.return_count { + Value::number(self.0.target_index as f64) + } else { + Value::Object( + self.0 + .target + .ok_or(RuntimeError::Invariant("flatten target missing"))?, + ) + }; return Ok(FlattenStep::Complete(Completion::Return( - if self.0.return_count { - Value::number(self.0.target_index as f64) - } else { - Value::Object( - self.0 - .target - .ok_or(RuntimeError::Invariant("flatten target missing"))?, - ) - }, + runtime.into_jsvalue(value)?, ))); }; if frame.next_index == frame.length { @@ -334,7 +353,9 @@ impl FlattenResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(FlattenStep::Complete(Completion::Throw(value))); + return Ok(FlattenStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !value { @@ -352,7 +373,9 @@ impl FlattenResume { match runtime.internal_is_array(self.0.realm, &element)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(FlattenStep::Complete(Completion::Throw(value))); + return Ok(FlattenStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } else { @@ -375,7 +398,11 @@ impl FlattenResume { } if self.0.target_index >= self.0.target_limit { return Ok(FlattenStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Type, "Array too long")?, + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "Array too long", + )?, ))); } self.0.phase = Phase::Define; @@ -410,7 +437,9 @@ impl FlattenResume { self.0.target_index, result, )? { - return Ok(FlattenStep::Complete(Completion::Throw(value))); + return Ok(FlattenStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.target_index += 1; self.next(runtime) @@ -433,7 +462,7 @@ pub(crate) fn finish( )? } FlattenStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } FlattenStep::Has { mut resume } => { @@ -457,8 +486,12 @@ pub(crate) fn finish( } FlattenStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -481,13 +514,13 @@ pub(crate) fn finish( struct FlattenStepPending { read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, has_object: Option, has_key: Option, species_source: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, define_object: Option, define_key: Option, define_descriptor: Option, @@ -502,7 +535,7 @@ impl FlattenStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: FlattenResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: FlattenResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -521,8 +554,8 @@ impl FlattenStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: FlattenResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -557,7 +590,7 @@ impl FlattenResume { .take() .expect("FlattenStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -592,14 +625,14 @@ impl FlattenResume { .take() .expect("FlattenStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("FlattenStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/array/indexed.rs b/src/engine/builtins/array/indexed.rs index 81f3a65c..08c4416c 100644 --- a/src/engine/builtins/array/indexed.rs +++ b/src/engine/builtins/array/indexed.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::ArraySearchKind, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -94,10 +94,15 @@ impl IndexedStep { "Array indexed method requires generic invocation", )); }; - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::request_read( object.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, @@ -107,7 +112,11 @@ impl IndexedStep { realm, kind, object, - arguments: arguments.readable.clone(), + arguments: arguments + .readable + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()?, actual: arguments.actual_arg_count, phase: Phase::Length, length: 0, @@ -143,16 +152,21 @@ impl IndexedResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(IndexedStep::Complete(Completion::Throw(value))), }; match self.0.phase { Phase::Length => { self.0.phase = Phase::LengthNumber; - Ok(IndexedStep::request_number(value, self)) + Ok(IndexedStep::request_number( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Read => match self.0.kind { - IndexedKind::At => Ok(IndexedStep::Complete(Completion::Return(value))), + IndexedKind::At => Ok(IndexedStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), IndexedKind::With | IndexedKind::ToReversed => { let output = if matches!(self.0.kind, IndexedKind::ToReversed) { self.0.length - self.0.index - 1 @@ -170,12 +184,13 @@ impl IndexedResume { search.strict_equal(&value) }; if found { + let result = if kind == ArraySearchKind::Includes { + Value::Bool(true) + } else { + Value::number(self.0.index as f64) + }; Ok(IndexedStep::Complete(Completion::Return( - if kind == ArraySearchKind::Includes { - Value::Bool(true) - } else { - Value::number(self.0.index as f64) - }, + runtime.into_jsvalue(result)?, ))) } else { self.advance(runtime) @@ -197,7 +212,9 @@ impl IndexedResume { let number = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(IndexedStep::Complete(Completion::Throw(value))); + return Ok(IndexedStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -260,7 +277,10 @@ impl IndexedResume { return self.bound(runtime, index + 1); } self.0.phase = Phase::Bound(index); - return Ok(IndexedStep::request_number(value, self)); + return Ok(IndexedStep::request_number( + runtime.into_jsvalue(value)?, + self, + )); } self.0.index = self.0.bounds[0]; self.0.end = self.0.length; @@ -272,9 +292,9 @@ impl IndexedResume { if self.0.index < 0 || self.0.index >= self.0.length { return Ok(IndexedStep::Complete( if matches!(self.0.kind, IndexedKind::At) { - Completion::Return(Value::Undefined) + Completion::Return(JsValue::Undefined) } else { - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, &format!("invalid array index: {}", self.0.index), @@ -286,7 +306,9 @@ impl IndexedResume { self.0.replacement = self.0.index; self.0.index = 0; if let Some(value) = self.allocate(runtime)? { - return Ok(IndexedStep::Complete(Completion::Throw(value))); + return Ok(IndexedStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } @@ -311,7 +333,9 @@ impl IndexedResume { } IndexedKind::ToReversed => { if let Some(value) = self.allocate(runtime)? { - return Ok(IndexedStep::Complete(Completion::Throw(value))); + return Ok(IndexedStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.index = self.0.length - 1; self.0.end = -1; @@ -350,7 +374,7 @@ impl IndexedResume { return Ok(IndexedStep::request_set( self.0.object.clone(), key, - self.argument(0), + runtime.into_jsvalue(self.argument(0))?, self, )); } @@ -372,7 +396,9 @@ impl IndexedResume { IndexedKind::At => Value::Undefined, _ => Value::Object(self.0.object), }; - Ok(IndexedStep::Complete(Completion::Return(value))) + Ok(IndexedStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))) } pub(crate) fn boolean( mut self, @@ -382,7 +408,9 @@ impl IndexedResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(IndexedStep::Complete(Completion::Throw(value))); + return Ok(IndexedStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -415,7 +443,9 @@ impl IndexedResume { return Err(RuntimeError::Invariant("Array indexed set phase mismatch")); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(IndexedStep::Complete(Completion::Throw(value))); + return Ok(IndexedStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.advance(runtime) } @@ -454,7 +484,7 @@ pub(crate) fn finish( )? } IndexedStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } IndexedStep::Has { mut resume } => { @@ -468,7 +498,7 @@ pub(crate) fn finish( IndexedStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -493,12 +523,12 @@ struct IndexedStepPending { copy_backwards: Option, read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, has_object: Option, has_key: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, } impl IndexedStep { pub(crate) fn request_copy( @@ -525,7 +555,7 @@ impl IndexedStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: IndexedResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: IndexedResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -541,7 +571,7 @@ impl IndexedStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: IndexedResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -600,7 +630,7 @@ impl IndexedResume { .take() .expect("IndexedStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -635,7 +665,7 @@ impl IndexedResume { .take() .expect("IndexedStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/mutation.rs b/src/engine/builtins/array/mutation.rs index cabb022c..76fe88a9 100644 --- a/src/engine/builtins/array/mutation.rs +++ b/src/engine/builtins/array/mutation.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::{ArrayPopKind, ArrayPushKind}, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -113,13 +113,13 @@ impl MutationStep { ( MutationKind::Push(_), [ - value @ (Value::Undefined - | Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_)), + value @ (JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_)), ], - ) => Some(value.clone()), + ) => Some(runtime.root_value(value)?), _ => None, }; let values = if inline.is_some() { @@ -129,9 +129,19 @@ impl MutationStep { ); Vec::new() } else { - values.to_vec() + values + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()? }; - Self::start_arguments(runtime, realm, kind, this_value.clone(), values, inline) + Self::start_arguments( + runtime, + realm, + kind, + runtime.root_value(this_value)?, + values, + inline, + ) } pub(crate) fn start_values( runtime: &Runtime, @@ -152,7 +162,11 @@ impl MutationStep { ) -> Result { let object = match runtime.native_to_object(realm, receiver)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; { @@ -170,7 +184,9 @@ impl MutationStep { _ => None, }; if let Some(value) = completed { - return Ok(Self::Complete(Completion::Return(value))); + return Ok(Self::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))); } } let action = MutationAction::Read( @@ -222,7 +238,7 @@ impl MutationResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(MutationAction::Complete(Completion::Throw(value))); } @@ -255,7 +271,9 @@ impl MutationResume { self.0.length = match result { NativeConversion::Value(number) => Runtime::length_from_number(number), NativeConversion::Throw(value) => { - return Ok(MutationAction::Complete(Completion::Throw(value))); + return Ok(MutationAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.kind { @@ -263,7 +281,7 @@ impl MutationResume { self.0.new_length = self.0.length.saturating_add(self.argument_count() as u64); if self.0.new_length > (1_u64 << 53) - 1 { return Ok(MutationAction::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "Array loo long", @@ -331,7 +349,7 @@ impl MutationResume { && self.0.new_length <= u64::from(u32::MAX) && matches!(runtime.array_length_state_if_genuine(&self.0.object)?, Some((length, true)) if u64::from(length) == self.0.new_length); if redundant { - self.complete() + self.complete(runtime) } else { self.write_length(runtime) } @@ -343,12 +361,13 @@ impl MutationResume { value: Value::number(self.0.new_length as f64), }) } - fn complete(&mut self) -> Result { + fn complete(&mut self, runtime: &Runtime) -> Result { + let value = match self.0.kind { + MutationKind::Push(_) => Value::number(self.0.new_length as f64), + MutationKind::Pop(_) => std::mem::replace(&mut self.0.result, Value::Undefined), + }; Ok(MutationAction::Complete(Completion::Return( - match self.0.kind { - MutationKind::Push(_) => Value::number(self.0.new_length as f64), - MutationKind::Pop(_) => std::mem::replace(&mut self.0.result, Value::Undefined), - }, + runtime.into_jsvalue(value)?, ))) } fn boolean_once( @@ -359,14 +378,16 @@ impl MutationResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(MutationAction::Complete(Completion::Throw(value))); + return Ok(MutationAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { Phase::DeleteLast => { if !value { return Ok(MutationAction::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "could not delete property", @@ -387,14 +408,16 @@ impl MutationResume { result: NativeConversion, ) -> Result { if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(MutationAction::Complete(Completion::Throw(value))); + return Ok(MutationAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } match self.0.phase { Phase::Write => { self.0.cursor += 1; self.write_next(runtime) } - Phase::LengthWrite => self.complete(), + Phase::LengthWrite => self.complete(runtime), _ => Err(RuntimeError::Invariant("Array mutation set phase mismatch")), } } @@ -452,7 +475,7 @@ impl MutationResume { )? { OrdinaryRead::Complete(value) => self.resume_once( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), )?, read => { return Ok(MutationStep::request_prepared_read(read, key, self)); @@ -510,7 +533,7 @@ impl MutationResume { runtime.internal_delete_property(self.0.realm, &self.0.object, &key)?; self.boolean_once(runtime, reply)? } - action => return Ok(self.wait(action)), + action => return self.wait(runtime, action), }; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( @@ -519,13 +542,15 @@ impl MutationResume { } } } - fn wait(self, action: MutationAction) -> MutationStep { - match action { + fn wait(self, runtime: &Runtime, action: MutationAction) -> Result { + Ok(match action { MutationAction::Complete(result) => MutationStep::Complete(result), MutationAction::Read(key) => { MutationStep::request_read(self.0.object.clone(), key, self) } - MutationAction::Number(value) => MutationStep::request_number(value, self), + MutationAction::Number(value) => { + MutationStep::request_number(runtime.into_jsvalue(value)?, self) + } MutationAction::Copy { to, from, @@ -534,13 +559,16 @@ impl MutationResume { } => { MutationStep::request_copy(self.0.object.clone(), to, from, count, backwards, self) } - MutationAction::Set { key, value } => { - MutationStep::request_set(self.0.object.clone(), key, value, self) - } + MutationAction::Set { key, value } => MutationStep::request_set( + self.0.object.clone(), + key, + runtime.into_jsvalue(value)?, + self, + ), MutationAction::Delete(key) => { MutationStep::request_delete(self.0.object.clone(), key, self) } - } + }) } } @@ -578,10 +606,12 @@ pub(crate) fn finish( let key = resume.take_prepared_read_key(); { let reply = match runtime.finish_prepared_read(realm, &key, read)? { - NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) + NativeConversion::Value(value) => Completion::Return( + runtime.into_jsvalue(value.unwrap_or(Value::Undefined))?, + ), + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) } - NativeConversion::Throw(value) => Completion::Throw(value), }; resume.resume(runtime, reply)? } @@ -611,7 +641,9 @@ pub(crate) fn finish( Completion::Return(_) => { NativeConversion::Value(InternalSetResult::Accepted) } - Completion::Throw(value) => NativeConversion::Throw(value), + Completion::Throw(value) => NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ), }; } SetStep::Complete(action) => break local_set_result(action)?, @@ -630,7 +662,7 @@ pub(crate) fn finish( )? } MutationStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } MutationStep::Copy { mut resume } => { @@ -653,7 +685,7 @@ pub(crate) fn finish( MutationStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -732,11 +764,11 @@ mod tests { ] { let array = runtime.new_array(context.realm).unwrap(); let invocation = NativeInvocation::Call { - this_value: Value::Object(array.clone()), + this_value: runtime.unroot_value(&Value::Object(array.clone())).unwrap(), }; let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![value.clone(), Value::Undefined], + readable: vec![runtime.unroot_value(&value).unwrap(), JsValue::Undefined], }; let step = MutationStep::start( &runtime, @@ -747,7 +779,7 @@ mod tests { ) .unwrap(); let result = finish(&runtime, context.realm, step).unwrap(); - assert!(matches!(result, Completion::Return(Value::Int(1)))); + assert!(matches!(result, Completion::Return(JsValue::Int(1)))); let key = runtime.property_key_for_index(0).unwrap(); let Completion::Return(actual) = runtime .get_property_in_realm(context.realm, &array, &key) @@ -755,15 +787,20 @@ mod tests { else { panic!("element read threw") }; - assert!(actual.same_quickjs_representation(&value)); + assert!( + runtime + .root_value(&actual) + .unwrap() + .same_quickjs_representation(&value) + ); } let array = runtime.new_array(context.realm).unwrap(); let invocation = NativeInvocation::Call { - this_value: Value::Object(array), + this_value: runtime.unroot_value(&Value::Object(array)).unwrap(), }; let arguments = NativeArguments { actual_arg_count: 0, - readable: vec![Value::Undefined], + readable: vec![JsValue::Undefined], }; let step = MutationStep::start( &runtime, @@ -775,7 +812,7 @@ mod tests { .unwrap(); assert!(matches!( finish(&runtime, context.realm, step).unwrap(), - Completion::Return(Value::Int(0)) + Completion::Return(JsValue::Int(0)) )); #[cfg(feature = "profiling")] assert_eq!( @@ -823,7 +860,7 @@ struct MutationStepPending { prepared_set_key: Option, read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, copy_object: Option, copy_to: Option, copy_from: Option, @@ -831,7 +868,7 @@ struct MutationStepPending { copy_backwards: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, delete_object: Option, delete_key: Option, } @@ -863,7 +900,7 @@ impl MutationStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: MutationResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: MutationResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -885,7 +922,7 @@ impl MutationStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: MutationResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -946,7 +983,7 @@ impl MutationResume { .take() .expect("MutationStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -1002,7 +1039,7 @@ impl MutationResume { .take() .expect("MutationStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/reverse.rs b/src/engine/builtins/array/reverse.rs index 52a2f81c..71e7af8b 100644 --- a/src/engine/builtins/array/reverse.rs +++ b/src/engine/builtins/array/reverse.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeInvocation}, }; pub(crate) enum ReverseStep { @@ -59,10 +59,15 @@ impl ReverseStep { "Array reverse requires generic invocation", )); }; - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::request_read( object.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, @@ -95,13 +100,16 @@ impl ReverseResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(ReverseStep::Complete(Completion::Throw(value))), }; match self.0.phase { Phase::Length => { self.0.phase = Phase::Number; - Ok(ReverseStep::request_number(value, self)) + Ok(ReverseStep::request_number( + runtime.into_jsvalue(value)?, + self, + )) } Phase::LowerRead => { self.0.lower_value = Some(value); @@ -129,16 +137,18 @@ impl ReverseResume { self.0.upper = match result { NativeConversion::Value(value) => Runtime::length_from_number(value).saturating_sub(1), NativeConversion::Throw(value) => { - return Ok(ReverseStep::Complete(Completion::Throw(value))); + return Ok(ReverseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.next(runtime) } fn next(mut self, runtime: &Runtime) -> Result { if self.0.lower >= self.0.upper { - return Ok(ReverseStep::Complete(Completion::Return(Value::Object( - self.0.object, - )))); + return Ok(ReverseStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.0.object))?, + ))); } self.0.phase = Phase::LowerHas; self.0.lower_value = None; @@ -163,7 +173,7 @@ impl ReverseResume { Ok(ReverseStep::request_set( self.0.object.clone(), runtime.property_key_for_index(self.0.lower)?, - value, + runtime.into_jsvalue(value)?, self, )) } else if self.0.lower_value.is_some() { @@ -182,7 +192,7 @@ impl ReverseResume { Ok(ReverseStep::request_set( self.0.object.clone(), runtime.property_key_for_index(self.0.upper)?, - value, + runtime.into_jsvalue(value)?, self, )) } else { @@ -206,7 +216,9 @@ impl ReverseResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReverseStep::Complete(Completion::Throw(value))); + return Ok(ReverseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -236,7 +248,7 @@ impl ReverseResume { Phase::LowerWrite | Phase::UpperWrite => { if !value { return Ok(ReverseStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "could not delete property", @@ -261,7 +273,9 @@ impl ReverseResume { result: NativeConversion, ) -> Result { if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(ReverseStep::Complete(Completion::Throw(value))); + return Ok(ReverseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } match self.0.phase { Phase::LowerWrite => self.write_upper(runtime), @@ -287,7 +301,7 @@ pub(crate) fn finish( )? } ReverseStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } ReverseStep::Has { mut resume } => { @@ -301,7 +315,7 @@ pub(crate) fn finish( ReverseStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -329,12 +343,12 @@ pub(crate) fn finish( struct ReverseStepPending { read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, has_object: Option, has_key: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, delete_object: Option, delete_key: Option, } @@ -348,7 +362,7 @@ impl ReverseStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: ReverseResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: ReverseResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -364,7 +378,7 @@ impl ReverseStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: ReverseResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -397,7 +411,7 @@ impl ReverseResume { .take() .expect("ReverseStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -432,7 +446,7 @@ impl ReverseResume { .take() .expect("ReverseStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/slice.rs b/src/engine/builtins/array/slice.rs index 572088bc..065e595b 100644 --- a/src/engine/builtins/array/slice.rs +++ b/src/engine/builtins/array/slice.rs @@ -9,7 +9,7 @@ use crate::engine::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, operations::{InternalDefineResult, InternalSetResult}, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -32,8 +32,8 @@ impl SliceKind { } } pub(crate) enum SliceStep { - Return(Value), - Throw(Value), + Return(JsValue), + Throw(JsValue), PreparedRead { resume: SliceResume }, PreparedHas { resume: SliceResume }, Read { resume: SliceResume }, @@ -53,7 +53,7 @@ pub(crate) struct SlicePending { key: Option, probe: Option, object: Option, - value: Option, + value: Option, source: Option, length: Option, descriptor: Option, @@ -128,10 +128,20 @@ impl SliceStep { "Array slice requires generic invocation", )); }; - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::complete(Completion::Throw(value))), - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; + let readable = arguments + .readable + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()?; Self::make_read( object.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, @@ -142,7 +152,7 @@ impl SliceStep { kind, phase: Phase::Length, object, - arguments: arguments.readable.clone(), + arguments: readable, actual: arguments.actual_arg_count, length: 0, start: 0, @@ -241,7 +251,7 @@ impl SliceResume { Ok(SliceStep::make_number(value, self)) } Phase::Species => { - let Value::Object(object) = value else { + let Value::Object(object) = runtime.root_and_release_jsvalue(value)? else { return Err(RuntimeError::Invariant( "ArraySpeciesCreate returned primitive", )); @@ -254,16 +264,17 @@ impl SliceResume { self.0.count, )? { - return Ok(SliceStep::complete(Completion::Return(Value::Object( - object, - )))); + return Ok(SliceStep::complete(Completion::Return( + runtime.into_jsvalue(Value::Object(object))?, + ))); } self.0.result = Some(object); self.collect(runtime) } Phase::Read => { if matches!(self.0.kind, SliceKind::ToSpliced) { - self.0.values[self.0.cursor as usize] = value; + self.0.values[self.0.cursor as usize] = + runtime.root_and_release_jsvalue(value)?; self.0.cursor += 1; return self.collect(runtime); } @@ -272,7 +283,7 @@ impl SliceResume { self.result()?, runtime.property_key_for_index(self.0.cursor)?, OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), + value: DescriptorField::Present(runtime.root_and_release_jsvalue(value)?), writable: DescriptorField::Present(true), enumerable: DescriptorField::Present(true), configurable: DescriptorField::Present(true), @@ -296,7 +307,9 @@ impl SliceResume { let number = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(SliceStep::complete(Completion::Throw(value))); + return Ok(SliceStep::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -306,7 +319,10 @@ impl SliceResume { return self.end(runtime); } self.0.phase = Phase::Start; - Ok(SliceStep::make_number(self.argument(0), self)) + Ok(SliceStep::make_number( + runtime.into_jsvalue(self.argument(0))?, + self, + )) } Phase::Start => { let mut index = Runtime::int64_from_number(number); @@ -340,7 +356,7 @@ impl SliceResume { }; if convert { self.0.phase = Phase::End; - return Ok(SliceStep::make_number(value, self)); + return Ok(SliceStep::make_number(runtime.into_jsvalue(value)?, self)); } self.0.count = if !matches!(self.0.kind, SliceKind::Slice) && self.0.actual == 0 { 0 @@ -354,7 +370,7 @@ impl SliceResume { self.0.new_length = (self.0.length - self.0.count).saturating_add(self.0.items); if self.0.new_length > (1_u64 << 53) - 1 { return Ok(SliceStep::complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, if matches!(self.0.kind, SliceKind::ToSpliced) { @@ -371,7 +387,9 @@ impl SliceResume { match runtime.native_allocate_fast_array_values(self.0.realm, self.0.new_length)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(SliceStep::complete(Completion::Throw(value))); + return Ok(SliceStep::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.collect(runtime) @@ -406,16 +424,18 @@ impl SliceResume { self.0.cursor += self.0.items; } if self.0.cursor == self.0.new_length { - return Ok(SliceStep::complete(Completion::Return(Value::Object( - runtime.new_array_from_values(self.0.realm, self.0.values)?, - )))); + return Ok(SliceStep::complete(Completion::Return( + runtime.into_jsvalue(Value::Object( + runtime.new_array_from_values(self.0.realm, self.0.values)?, + ))?, + ))); } } else if self.0.cursor == self.0.count { self.0.phase = Phase::ResultLength; return Ok(SliceStep::make_set( self.result()?, runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, - Value::number(self.0.count as f64), + runtime.into_jsvalue(Value::number(self.0.count as f64))?, self, )); } @@ -451,7 +471,10 @@ impl SliceResume { crate::engine::api::profiling::record_owned_execution_event( "array_slice_local_read", ); - value.unwrap_or(Value::Undefined) + match value { + Some(value) => runtime.root_and_release_jsvalue(value)?, + None => Value::Undefined, + } } read => { return Ok(SliceStep::make_preparedread(read, key, self)); @@ -494,7 +517,9 @@ impl SliceResume { self.0.cursor, result, )? { - return Ok(SliceStep::complete(Completion::Throw(value))); + return Ok(SliceStep::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.cursor += 1; #[cfg(feature = "profiling")] @@ -512,7 +537,9 @@ impl SliceResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(SliceStep::complete(Completion::Throw(value))); + return Ok(SliceStep::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -531,7 +558,7 @@ impl SliceResume { Phase::Delete => { if !value { return Ok(SliceStep::complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "could not delete property", @@ -556,14 +583,16 @@ impl SliceResume { if let Some(value) = runtime.finish_create_indexed_data_property(self.0.realm, self.0.cursor, result)? { - return Ok(SliceStep::complete(Completion::Throw(value))); + return Ok(SliceStep::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.cursor += 1; self.collect(runtime) } fn mutate(mut self, runtime: &Runtime) -> Result { if matches!(self.0.kind, SliceKind::Slice) { - return self.complete(); + return self.complete(runtime); } if self.0.items != self.0.count { self.0.phase = Phase::Copy; @@ -598,7 +627,7 @@ impl SliceResume { return Ok(SliceStep::make_set( self.0.object.clone(), runtime.property_key_for_index(self.0.start + self.0.cursor)?, - self.argument(self.0.cursor as usize + 2), + runtime.into_jsvalue(self.argument(self.0.cursor as usize + 2))?, self, )); } @@ -606,7 +635,7 @@ impl SliceResume { Ok(SliceStep::make_set( self.0.object.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, - Value::number(self.0.new_length as f64), + runtime.into_jsvalue(Value::number(self.0.new_length as f64))?, self, )) } @@ -617,7 +646,9 @@ impl SliceResume { result: NativeConversion, ) -> Result { if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(SliceStep::complete(Completion::Throw(value))); + return Ok(SliceStep::complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } match self.0.phase { Phase::ResultLength => self.mutate(runtime), @@ -625,14 +656,14 @@ impl SliceResume { self.0.cursor += 1; self.insert(runtime) } - Phase::FinalLength => self.complete(), + Phase::FinalLength => self.complete(runtime), _ => Err(RuntimeError::Invariant("Array slice set phase mismatch")), } } - fn complete(self) -> Result { - Ok(SliceStep::complete(Completion::Return(Value::Object( - self.result()?, - )))) + fn complete(self, runtime: &Runtime) -> Result { + Ok(SliceStep::complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.result()?))?, + ))) } } mod local; @@ -649,10 +680,12 @@ pub(crate) fn finish( let (read, key) = resume.take_preparedread(); { let completion = match runtime.finish_prepared_read(realm, &key, read)? { - NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) + NativeConversion::Value(value) => Completion::Return( + runtime.into_jsvalue(value.unwrap_or(Value::Undefined))?, + ), + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) } - NativeConversion::Throw(value) => Completion::Throw(value), }; resume.resume(runtime, completion)? } @@ -670,6 +703,7 @@ pub(crate) fn finish( } SliceStep::Number { mut resume } => { let (value,) = resume.take_number(); + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } SliceStep::Species { mut resume } => { @@ -692,6 +726,7 @@ pub(crate) fn finish( } SliceStep::Set { mut resume } => { let (object, key, value) = resume.take_set(); + let value = runtime.root_and_release_jsvalue(value)?; { let result = runtime.internal_set( realm, @@ -760,7 +795,7 @@ fn slice_resume_keeps_one_resident_owner_across_number_transitions() { }; assert_eq!(&*resume.0 as *const SliceResumeState, address); let (pending_value,) = resume.take_number(); - assert!(matches!(pending_value, Value::Object(_))); + assert!(matches!(pending_value, JsValue::Object(_))); assert!(resume.0.pending.value.is_none()); let SliceStep::Number { mut resume, .. } = resume .number_once(&runtime, NativeConversion::Value(1.0)) @@ -770,7 +805,7 @@ fn slice_resume_keeps_one_resident_owner_across_number_transitions() { }; assert_eq!(&*resume.0 as *const SliceResumeState, address); let (pending_value,) = resume.take_number(); - assert!(matches!(pending_value, Value::Object(_))); + assert!(matches!(pending_value, JsValue::Object(_))); assert!(resume.0.pending.value.is_none()); assert_eq!(resume.0.start, 1); } @@ -799,7 +834,7 @@ impl SliceStep { resume.0.pending.key = Some(key); Self::Read { resume } } - fn make_number(value: Value, mut resume: SliceResume) -> Self { + fn make_number(value: JsValue, mut resume: SliceResume) -> Self { resume.0.pending.value = Some(value); Self::Number { resume } } @@ -823,7 +858,7 @@ impl SliceStep { fn make_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: SliceResume, ) -> Self { resume.0.pending.object = Some(object); @@ -893,7 +928,7 @@ impl SliceResume { self.0.pending.key.take().expect("slice Read lost key"), ) } - pub(crate) fn take_number(&mut self) -> (Value,) { + pub(crate) fn take_number(&mut self) -> (JsValue,) { (self .0 .pending @@ -930,7 +965,7 @@ impl SliceResume { .expect("slice Define lost descriptor"), ) } - pub(crate) fn take_set(&mut self) -> (ObjectRef, PropertyKey, Value) { + pub(crate) fn take_set(&mut self) -> (ObjectRef, PropertyKey, JsValue) { ( self.0.pending.object.take().expect("slice Set lost object"), self.0.pending.key.take().expect("slice Set lost key"), diff --git a/src/engine/builtins/array/slice/local.rs b/src/engine/builtins/array/slice/local.rs index a54367bd..c5f6f95a 100644 --- a/src/engine/builtins/array/slice/local.rs +++ b/src/engine/builtins/array/slice/local.rs @@ -25,16 +25,17 @@ impl SliceStep { ); resume.resume_once( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), )? } read => return Ok(Self::make_preparedread(read, key, resume)), } } Self::Number { mut resume } - if !matches!(resume.0.pending.value.as_ref(), Some(Value::Object(_))) => + if !matches!(resume.0.pending.value.as_ref(), Some(JsValue::Object(_))) => { let (value,) = resume.take_number(); + let value = runtime.root_and_release_jsvalue(value)?; let NumberStep::Complete(result) = NumberStep::start(runtime, realm, value)? else { return Err(RuntimeError::Invariant( diff --git a/src/engine/builtins/array/sort.rs b/src/engine/builtins/array/sort.rs index b1d09bd2..515867fb 100644 --- a/src/engine/builtins/array/sort.rs +++ b/src/engine/builtins/array/sort.rs @@ -7,7 +7,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -78,17 +78,26 @@ impl SortStep { ) -> Result { let comparator = match runtime.native_sort_comparator(realm, arguments)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let NativeInvocation::Call { this_value } = invocation else { return Err(RuntimeError::Invariant( "Array sort requires generic invocation", )); }; - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::request_read( object.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, @@ -129,13 +138,13 @@ impl SortResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(SortStep::Complete(Completion::Throw(value))), }; match self.0.phase { Phase::Length => { self.0.phase = Phase::LengthNumber; - Ok(SortStep::request_number(value, self)) + Ok(SortStep::request_number(runtime.into_jsvalue(value)?, self)) } Phase::CollectRead => { self.collect_value(value); @@ -147,7 +156,7 @@ impl SortResume { return self.compared(runtime, order_from_number(f64::from(value))); } self.0.phase = Phase::CompareNumber; - Ok(SortStep::request_number(value, self)) + Ok(SortStep::request_number(runtime.into_jsvalue(value)?, self)) } _ => Err(RuntimeError::Invariant("Array sort value phase mismatch")), } @@ -160,7 +169,9 @@ impl SortResume { let number = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(SortStep::Complete(Completion::Throw(value))); + return Ok(SortStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -172,7 +183,9 @@ impl SortResume { { NativeConversion::Value(values) => values, NativeConversion::Throw(value) => { - return Ok(SortStep::Complete(Completion::Throw(value))); + return Ok(SortStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; } @@ -227,7 +240,9 @@ impl SortResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(SortStep::Complete(Completion::Throw(value))); + return Ok(SortStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -246,7 +261,7 @@ impl SortResume { Phase::Delete => { if !value { return Ok(SortStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "could not delete property", @@ -292,8 +307,8 @@ impl SortResume { return Ok(SortStep::request_call( callable, vec![ - self.0.slots[left].value.clone(), - self.0.slots[right].value.clone(), + runtime.into_jsvalue(self.0.slots[left].value.clone())?, + runtime.into_jsvalue(self.0.slots[right].value.clone())?, ], self, )); @@ -321,14 +336,14 @@ impl SortResume { if self.0.slots[self.0.left].cached_string.is_none() { self.0.phase = Phase::LeftString; return Ok(SortStep::request_string( - self.0.slots[self.0.left].value.clone(), + runtime.into_jsvalue(self.0.slots[self.0.left].value.clone())?, self, )); } if self.0.slots[self.0.right].cached_string.is_none() { self.0.phase = Phase::RightString; return Ok(SortStep::request_string( - self.0.slots[self.0.right].value.clone(), + runtime.into_jsvalue(self.0.slots[self.0.right].value.clone())?, self, )); } @@ -358,7 +373,9 @@ impl SortResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(SortStep::Complete(Completion::Throw(value))); + return Ok(SortStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let index = match self.0.phase { @@ -396,7 +413,7 @@ impl SortResume { return Ok(SortStep::request_set( self.0.object.clone(), runtime.property_key_for_index(self.0.cursor)?, - value, + runtime.into_jsvalue(value)?, self, )); } @@ -406,7 +423,7 @@ impl SortResume { return Ok(SortStep::request_set( self.0.object.clone(), runtime.property_key_for_index(self.0.cursor)?, - Value::Undefined, + JsValue::Undefined, self, )); } @@ -418,9 +435,9 @@ impl SortResume { self, )); } - Ok(SortStep::Complete(Completion::Return(Value::Object( - self.0.object, - )))) + Ok(SortStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.0.object))?, + ))) } pub(crate) fn set( mut self, @@ -432,7 +449,9 @@ impl SortResume { return Err(RuntimeError::Invariant("Array sort set phase mismatch")); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(SortStep::Complete(Completion::Throw(value))); + return Ok(SortStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.cursor += 1; self.write_next(runtime) @@ -464,11 +483,11 @@ pub(crate) fn finish( )? } SortStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } SortStep::String { mut resume } => { - let value = resume.take_string_value(); + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } SortStep::Has { mut resume } => { @@ -481,7 +500,11 @@ pub(crate) fn finish( } SortStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let arguments = resume.take_call_arguments(); + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, Value::Undefined, &arguments)?, @@ -490,7 +513,7 @@ pub(crate) fn finish( SortStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; { let result = runtime.internal_set( realm, @@ -518,15 +541,15 @@ pub(crate) fn finish( struct SortStepPending { read_object: Option, read_key: Option, - number_value: Option, - string_value: Option, + number_value: Option, + string_value: Option, has_object: Option, has_key: Option, call_callable: Option, - call_arguments: Option>, + call_arguments: Option>, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, delete_object: Option, delete_key: Option, } @@ -540,11 +563,11 @@ impl SortStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: SortResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: SortResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } - pub(crate) fn request_string(value: Value, mut resume: SortResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: SortResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } @@ -555,7 +578,7 @@ impl SortStep { } pub(crate) fn request_call( callable: CallableRef, - arguments: Vec, + arguments: Vec, mut resume: SortResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -565,7 +588,7 @@ impl SortStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: SortResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -598,14 +621,14 @@ impl SortResume { .take() .expect("SortStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value .take() .expect("SortStep Number value") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value @@ -633,7 +656,7 @@ impl SortResume { .take() .expect("SortStep Call callable") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -654,7 +677,7 @@ impl SortResume { .take() .expect("SortStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/array/species.rs b/src/engine/builtins/array/species.rs index e5a46684..bc10551c 100644 --- a/src/engine/builtins/array/species.rs +++ b/src/engine/builtins/array/species.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::ConstructorRef}, }; pub(crate) enum SpeciesStep { @@ -15,7 +15,7 @@ pub(crate) enum SpeciesStep { }, Construct { target: ConstructorRef, - arguments: Vec, + arguments: Vec, }, } enum Phase { @@ -35,7 +35,9 @@ impl SpeciesStep { length: u64, ) -> Result { match runtime.internal_is_array(realm, &Value::Object(source.clone()))? { - NativeConversion::Throw(value) => Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), NativeConversion::Value(false) => allocate(runtime, realm, length), NativeConversion::Value(true) => Ok(Self::Read { object: source.clone(), @@ -64,7 +66,7 @@ impl SpeciesResume { result: Completion, ) -> Result { let mut constructor = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(SpeciesStep::Complete(Completion::Throw(value))), }; if matches!(self.phase, Phase::Constructor) { @@ -75,7 +77,9 @@ impl SpeciesResume { match runtime.function_realm_from_value(self.realm, &constructor)? { NativeConversion::Value(realm) => realm, NativeConversion::Throw(value) => { - return Ok(SpeciesStep::Complete(Completion::Throw(value))); + return Ok(SpeciesStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if constructor_realm != self.realm @@ -108,9 +112,11 @@ impl SpeciesResume { match runtime.constructor_from_value(self.realm, constructor)? { NativeConversion::Value(target) => Ok(SpeciesStep::Construct { target, - arguments: vec![Value::number(self.length as f64)], + arguments: vec![runtime.into_jsvalue(Value::number(self.length as f64))?], }), - NativeConversion::Throw(value) => Ok(SpeciesStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(SpeciesStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), } } } @@ -131,6 +137,10 @@ pub(crate) fn finish( runtime.get_property_in_realm(realm, &object, &key)?, )?, SpeciesStep::Construct { target, arguments } => { + let arguments = arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; return runtime.construct_constructor_internal(realm, &target, &target, &arguments); } }; diff --git a/src/engine/builtins/array/string.rs b/src/engine/builtins/array/string.rs index b319d7d6..b1f34beb 100644 --- a/src/engine/builtins/array/string.rs +++ b/src/engine/builtins/array/string.rs @@ -6,7 +6,9 @@ use crate::engine::{ builtins::native::ArrayJoinKind, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey}, - value::{JsString, JsStringBuilder, JsStringError, Value, conversion::NativeConversion}, + value::{ + JsString, JsStringBuilder, JsStringError, JsValue, Value, conversion::NativeConversion, + }, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -32,7 +34,7 @@ pub(crate) enum ArrayStringStep { Number { resume: ArrayStringResume }, String { resume: ArrayStringResume }, Call { resume: ArrayStringResume }, - ObjectTag { receiver: Value }, + ObjectTag { receiver: JsValue }, } enum Phase { Length, @@ -64,13 +66,13 @@ pub(crate) struct ArrayStringResumeState { kind: ArrayStringKind, object: ObjectRef, phase: Phase, - separator_value: Value, + separator_value: JsValue, separator: JsString, output: JsStringBuilder, separator_error: Option, length: u64, index: u64, - element: Value, + element: JsValue, } impl ArrayStringStep { pub(crate) fn start( @@ -86,13 +88,18 @@ impl ArrayStringStep { "Array string method requires generic invocation", )); }; - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; let to_string = matches!(kind, ArrayStringKind::ToString); Ok({ - let __pending_field_receiver = Value::Object(object.clone()); + let __pending_field_receiver = JsValue::Object(object.clone().into_handle()); let __pending_field_key = runtime.intern_property_key(if to_string { "join" } else { "length" })?; let __pending_field_resume = ArrayStringResume(Box::new(ArrayStringResumeState { @@ -105,17 +112,16 @@ impl ArrayStringStep { } else { Phase::Length }, - separator_value: arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined), + separator_value: match arguments.readable.first() { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, + }, separator: JsString::from_static(","), output: JsStringBuilder::with_limit(0, string_limit), separator_error: None, length: 0, index: 0, - element: Value::Undefined, + element: JsValue::Undefined, })); Self::request_read( __pending_field_receiver, @@ -147,7 +153,7 @@ impl ArrayStringResume { }) } Phase::Element => { - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { self.0.index += 1; return self.next(runtime); } @@ -155,7 +161,7 @@ impl ArrayStringResume { self.0.kind, ArrayStringKind::Join(ArrayJoinKind::ToLocaleString) ) { - self.0.element = value.clone(); + self.0.element = runtime.dup_jsvalue(&value)?; self.0.phase = Phase::LocaleMethod; return Ok({ let __pending_field_receiver = value; @@ -173,13 +179,10 @@ impl ArrayStringResume { self.element_string(value) } Phase::LocaleMethod => { - let callable = match value { - Value::Object(object) => runtime.as_callable(&object)?, - _ => None, - }; + let callable = callable(runtime, &value)?; let Some(callable) = callable else { return Ok(ArrayStringStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not a function", @@ -189,7 +192,7 @@ impl ArrayStringResume { self.0.phase = Phase::LocaleResult; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = self.0.element.clone(); + let __pending_field_receiver = runtime.dup_jsvalue(&self.0.element)?; let __pending_field_resume = self; ArrayStringStep::request_call( __pending_field_callable, @@ -199,19 +202,17 @@ impl ArrayStringResume { }) } Phase::LocaleResult => { - self.0.element = Value::Undefined; + self.0.element = JsValue::Undefined; self.element_string(value) } Phase::JoinMethod => { - let callable = match value { - Value::Object(object) => runtime.as_callable(&object)?, - _ => None, - }; + let callable = callable(runtime, &value)?; if let Some(callable) = callable { self.0.phase = Phase::JoinResult; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Object(self.0.object.clone()); + let __pending_field_receiver = + JsValue::Object(self.0.object.clone().into_handle()); let __pending_field_resume = self; ArrayStringStep::request_call( __pending_field_callable, @@ -221,7 +222,7 @@ impl ArrayStringResume { }) } else { Ok(ArrayStringStep::ObjectTag { - receiver: Value::Object(self.0.object), + receiver: JsValue::Object(self.0.object.into_handle()), }) } } @@ -229,7 +230,7 @@ impl ArrayStringResume { _ => Err(RuntimeError::Invariant("Array string value phase mismatch")), } } - fn element_string(mut self, value: Value) -> Result { + fn element_string(mut self, value: JsValue) -> Result { if let Some(error) = self.0.separator_error { return Err(error.into()); } @@ -253,14 +254,16 @@ impl ArrayStringResume { self.0.length = match result { NativeConversion::Value(value) => Runtime::length_from_number(value), NativeConversion::Throw(value) => { - return Ok(ArrayStringStep::Complete(Completion::Throw(value))); + return Ok(ArrayStringStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if matches!(self.0.kind, ArrayStringKind::Join(ArrayJoinKind::Join)) - && !matches!(self.0.separator_value, Value::Undefined) + && !matches!(self.0.separator_value, JsValue::Undefined) { self.0.phase = Phase::Separator; - let value = std::mem::replace(&mut self.0.separator_value, Value::Undefined); + let value = std::mem::replace(&mut self.0.separator_value, JsValue::Undefined); return Ok({ let __pending_field_value = value; let __pending_field_resume = self; @@ -277,7 +280,9 @@ impl ArrayStringResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ArrayStringStep::Complete(Completion::Throw(value))); + return Ok(ArrayStringStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -300,7 +305,7 @@ impl ArrayStringResume { return Err(error.into()); } return Ok(ArrayStringStep::Complete(Completion::Return( - Value::String(self.0.output.finish()?), + runtime.into_jsvalue(Value::String(self.0.output.finish()?))?, ))); } if self.0.index != 0 @@ -310,7 +315,7 @@ impl ArrayStringResume { } self.0.phase = Phase::Element; Ok({ - let __pending_field_receiver = Value::Object(self.0.object.clone()); + let __pending_field_receiver = JsValue::Object(self.0.object.clone().into_handle()); let __pending_field_key = runtime.property_key_for_index(u64::from(self.0.index as u32))?; let __pending_field_resume = self; @@ -322,6 +327,15 @@ impl ArrayStringResume { }) } } +fn callable(runtime: &Runtime, value: &JsValue) -> Result, RuntimeError> { + Ok(match value { + JsValue::Object(id) => { + runtime.as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)? + } + _ => None, + }) +} + pub(crate) fn finish( runtime: &Runtime, realm: ContextId, @@ -331,7 +345,7 @@ pub(crate) fn finish( step = match step { ArrayStringStep::Complete(result) => return Ok(result), ArrayStringStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -339,16 +353,16 @@ pub(crate) fn finish( )? } ArrayStringStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } ArrayStringStep::String { mut resume } => { - let value = resume.take_string_value(); + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } ArrayStringStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &[])?, @@ -368,16 +382,16 @@ pub(crate) fn finish( #[derive(Default)] struct ArrayStringStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, - number_value: Option, - string_value: Option, + number_value: Option, + string_value: Option, call_callable: Option, - call_receiver: Option, + call_receiver: Option, } impl ArrayStringStep { pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: ArrayStringResume, ) -> Self { @@ -385,17 +399,17 @@ impl ArrayStringStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: ArrayStringResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: ArrayStringResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } - pub(crate) fn request_string(value: Value, mut resume: ArrayStringResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: ArrayStringResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, + receiver: JsValue, mut resume: ArrayStringResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -404,7 +418,7 @@ impl ArrayStringStep { } } impl ArrayStringResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -418,14 +432,14 @@ impl ArrayStringResume { .take() .expect("ArrayStringStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value .take() .expect("ArrayStringStep Number value") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value @@ -439,7 +453,7 @@ impl ArrayStringResume { .take() .expect("ArrayStringStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver diff --git a/src/engine/builtins/array/tests.rs b/src/engine/builtins/array/tests.rs index 54e656ae..ab5e5dca 100644 --- a/src/engine/builtins/array/tests.rs +++ b/src/engine/builtins/array/tests.rs @@ -1,4 +1,5 @@ use crate::engine::api::Context; +use crate::engine::atom::AtomIdx; use crate::engine::heap::RawValue; use super::*; @@ -109,7 +110,8 @@ fn array_unscopables_autoinit_retains_then_releases_its_realm_edge() { let state = runtime.0.state.borrow(); let object = state.heap.object(array_prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::AutoInit( @@ -162,7 +164,8 @@ fn array_unscopables_metadata_and_delete_preserve_lazy_state() { let state = runtime.0.state.borrow(); let object = state.heap.object(array_prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::AutoInit( @@ -185,7 +188,7 @@ fn array_unscopables_metadata_and_delete_preserve_lazy_state() { ); let object = state.heap.object(array_prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - assert!(shape.find(key.atom()).is_none()); + assert!(shape.find(AtomIdx::from_raw(key.atom().raw())).is_none()); } fn eval_object(context: &mut Context, source: &str) -> ObjectRef { diff --git a/src/engine/builtins/array_buffer.rs b/src/engine/builtins/array_buffer.rs index d2f1b8ab..26a66b37 100644 --- a/src/engine/builtins/array_buffer.rs +++ b/src/engine/builtins/array_buffer.rs @@ -23,7 +23,7 @@ use crate::engine::object::{ WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -271,14 +271,14 @@ impl Runtime { max_byte_length: Option, ) -> Result { if length > MAX_ARRAY_BUFFER_LENGTH { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid array buffer length", )?)); } if max_byte_length.is_some_and(|maximum| maximum > MAX_ARRAY_BUFFER_LENGTH) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid max array buffer length", @@ -293,13 +293,15 @@ impl Runtime { .map_err(|_| RuntimeError::Invariant("validated ArrayBuffer maximum overflowed u32"))?; let Some(object) = self.new_array_buffer_object(&prototype, length, max_byte_length)? else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?)); }; - Ok(Completion::Return(Value::Object(object))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(object))?, + )) } pub(in crate::engine::builtins) fn call_array_buffer_is_view( @@ -317,17 +319,17 @@ impl Runtime { "ArrayBuffer.isView argument was not padded", )); }; - let is_view = if let Value::Object(object) = value { + let is_view = if let JsValue::Object(id) = value { let state = self.0.state.borrow(); matches!( - state.heap.object(object.object_id())?.payload, + state.heap.object(*id)?.payload, ObjectPayload::DataView(_) | ObjectPayload::TypedArray(_) ) } else { false }; // Proxies intentionally do not forward this internal-slot brand test. - Ok(Completion::Return(Value::Bool(is_view))) + Ok(Completion::Return(JsValue::Bool(is_view))) } pub(in crate::engine::builtins) fn call_array_buffer_species( @@ -339,7 +341,7 @@ impl Runtime { "ArrayBuffer species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } pub(in crate::engine::builtins) fn call_array_buffer_getter( @@ -353,9 +355,12 @@ impl Runtime { "ArrayBuffer prototype getter received a non-getter invocation", )); }; - let object = match self.require_array_buffer_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let object = match self.require_array_buffer_borrowed(realm, &this_value)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let snapshot = self.array_buffer_snapshot(object)?; let value = match kind { @@ -381,7 +386,7 @@ impl Runtime { )); } }; - Ok(Completion::Return(value)) + Ok(Completion::Return(self.into_jsvalue(value)?)) } fn call_array_buffer_resize( @@ -410,21 +415,21 @@ impl Runtime { ) -> Result { let current = self.array_buffer_snapshot(&object)?; if current.detached { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", )?)); } let Some(maximum) = current.max_byte_length else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "array buffer is not resizable", )?)); }; if new_length < 0 || new_length > i64::from(maximum) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid array buffer length", @@ -439,13 +444,13 @@ impl Runtime { .heap .resize_array_buffer_bytes(object.object_id(), new_length)?; if !resized { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?)); } - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } fn call_array_buffer_slice( @@ -514,7 +519,7 @@ impl Runtime { new_length: u32, ) -> Result { if target.object_id() == source.object_id() { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot use identical ArrayBuffer", @@ -523,7 +528,7 @@ impl Runtime { let target_snapshot = match self.array_buffer_snapshot_if_branded(&target)? { Some(snapshot) => snapshot, None => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer object expected", @@ -531,14 +536,14 @@ impl Runtime { } }; if target_snapshot.detached { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", )?)); } if target_snapshot.byte_length < new_length { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "new ArrayBuffer is too small", @@ -565,7 +570,7 @@ impl Runtime { !data.detached && end <= data.bytes.len() }; if !source_is_live { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", @@ -577,7 +582,9 @@ impl Runtime { start, new_length_usize, )?; - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target))?, + )) } fn call_array_buffer_transfer( @@ -607,7 +614,7 @@ impl Runtime { ) -> Result { let current = self.array_buffer_snapshot(&source)?; if current.detached { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", @@ -619,14 +626,14 @@ impl Runtime { current.max_byte_length }; if result_maximum.is_some_and(|maximum| new_length > u64::from(maximum)) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "invalid array buffer length", )?)); } if new_length > MAX_ARRAY_BUFFER_LENGTH { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid array buffer length", @@ -636,7 +643,7 @@ impl Runtime { .map_err(|_| RuntimeError::Invariant("transfer length overflowed usize"))?; let prototype = self.array_buffer_default_prototype(realm)?; let Some(target) = self.new_array_buffer_object(&prototype, 0, result_maximum)? else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", @@ -648,13 +655,15 @@ impl Runtime { new_length, )?; if !transferred { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?)); } - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target))?, + )) } fn array_buffer_default_prototype(&self, realm: ContextId) -> Result { @@ -863,8 +872,9 @@ impl Runtime { let value = arguments.readable.first().ok_or(RuntimeError::Invariant( "Test262 detachArrayBuffer argument was not padded", ))?; - self.detach_array_buffer_value(value)?; - Ok(Completion::Return(Value::Undefined)) + let value = self.root_value(value)?; + self.detach_array_buffer_value(&value)?; + Ok(Completion::Return(JsValue::Undefined)) } } diff --git a/src/engine/builtins/array_buffer/constructor.rs b/src/engine/builtins/array_buffer/constructor.rs index 9bee4ed2..18a71573 100644 --- a/src/engine/builtins/array_buffer/constructor.rs +++ b/src/engine/builtins/array_buffer/constructor.rs @@ -4,7 +4,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{ @@ -16,7 +16,7 @@ use crate::engine::{ pub(crate) enum BufferConstructorStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: BufferConstructorResume, }, Read { @@ -25,7 +25,7 @@ pub(crate) enum BufferConstructorStep { resume: BufferConstructorResume, }, Prototype { - new_target: Value, + new_target: JsValue, resume: BufferConstructorResume, }, } @@ -84,28 +84,25 @@ impl BufferConstructorStep { "ArrayBuffer constructor did not receive a constructor invocation", )); }; - let value = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "ArrayBuffer length argument was not padded", - ))? - .clone(); + let value = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("ArrayBuffer length argument was not padded"), + )?)?; let options = if arguments.actual_arg_count >= 2 { match arguments.readable.get(1) { - Some(Value::Object(object)) => Some(object.clone()), + Some(JsValue::Object(id)) => { + Some(ObjectRef::from_borrowed_handle(runtime.clone(), *id)?) + } _ => None, } } else { None }; - let _ = runtime; Ok(Self::Primitive { value, resume: BufferConstructorResume(Box::new(BufferConstructorResumeState { realm, shared: false, - new_target: new_target.clone(), + new_target: runtime.root_value(new_target)?, options, phase: ConstructorPhase::Length, })), @@ -113,15 +110,20 @@ impl BufferConstructorStep { } } impl BufferConstructorResume { - fn lookup(mut self, length: u64, maximum: Option) -> BufferConstructorStep { - BufferConstructorStep::Prototype { - new_target: self.0.new_target.clone(), + fn lookup( + mut self, + runtime: &Runtime, + length: u64, + maximum: Option, + ) -> Result { + Ok(BufferConstructorStep::Prototype { + new_target: runtime.into_jsvalue(self.0.new_target.clone())?, resume: { let updated_0 = ConstructorPhase::Prototype { length, maximum }; self.0.phase = updated_0; self }, - } + }) } pub(crate) fn resume( mut self, @@ -129,7 +131,7 @@ impl BufferConstructorResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(BufferConstructorStep::Complete(Completion::Throw(value))); } @@ -144,7 +146,9 @@ impl BufferConstructorResume { let length = match runtime.native_to_index(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(BufferConstructorStep::Complete(Completion::Throw(value))); + return Ok(BufferConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if let Some(options) = &self.0.options { @@ -160,15 +164,15 @@ impl BufferConstructorResume { }, }) } else { - Ok(self.lookup(length, None)) + self.lookup(runtime, length, None) } } ConstructorPhase::Maximum(length) => { if matches!(value, Value::Undefined) { - Ok(self.lookup(length, None)) + self.lookup(runtime, length, None) } else { Ok(BufferConstructorStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: { let updated_0 = ConstructorPhase::MaximumNumber(length); self.0.phase = updated_0; @@ -186,19 +190,21 @@ impl BufferConstructorResume { let maximum = match runtime.native_to_int64(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(BufferConstructorStep::Complete(Completion::Throw(value))); + return Ok(BufferConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if maximum > MAX_SAFE_INTEGER_I64 || length > maximum as u64 { return Ok(BufferConstructorStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid array buffer max length", )?, ))); } - Ok(self.lookup(length, Some(maximum as u64))) + self.lookup(runtime, length, Some(maximum as u64)) } ConstructorPhase::Prototype { .. } => Err(RuntimeError::Invariant( "ArrayBuffer prototype request received an untyped reply", @@ -220,7 +226,9 @@ impl BufferConstructorResume { } } NativeConversion::Throw(value) => { - return Ok(BufferConstructorStep::Complete(Completion::Throw(value))); + return Ok(BufferConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let ConstructorPhase::Prototype { length, maximum } = self.0.phase else { @@ -249,8 +257,8 @@ pub(in crate::engine::builtins) fn finish( step = match step { BufferConstructorStep::Complete(result) => return Ok(result), BufferConstructorStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; @@ -264,14 +272,17 @@ pub(in crate::engine::builtins) fn finish( runtime, runtime.get_property_in_realm(realm, &object, &key)?, )?, - BufferConstructorStep::Prototype { new_target, resume } => resume.prototype( - runtime, - finish_source( + BufferConstructorStep::Prototype { new_target, resume } => { + let new_target = runtime.root_and_release_jsvalue(new_target)?; + resume.prototype( runtime, - realm, - ProtoSourceStep::start(runtime, realm, new_target)?, - )?, - )?, + finish_source( + runtime, + realm, + ProtoSourceStep::start(runtime, realm, new_target)?, + )?, + )? + } }; } } diff --git a/src/engine/builtins/array_buffer/data_view.rs b/src/engine/builtins/array_buffer/data_view.rs index 8bc2d280..e974bbab 100644 --- a/src/engine/builtins/array_buffer/data_view.rs +++ b/src/engine/builtins/array_buffer/data_view.rs @@ -20,7 +20,7 @@ use crate::engine::{ object::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{ @@ -202,7 +202,7 @@ impl Runtime { ) -> Result { let current = self.snapshot_buffer_access(buffer.object_id())?.state; if current.detached { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", @@ -215,7 +215,7 @@ impl Runtime { .is_none_or(|end| end > current.byte_length) }); if bounds_are_invalid { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid byteOffset or byteLength", @@ -224,7 +224,9 @@ impl Runtime { let object = self.new_data_view_object(&prototype, &buffer, byte_offset, fixed_byte_length)?; - Ok(Completion::Return(Value::Object(object))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(object))?, + )) } pub(in crate::engine::builtins) fn call_data_view_getter( @@ -238,19 +240,24 @@ impl Runtime { "DataView prototype getter received a non-getter invocation", )); }; - let object = match self.require_data_view_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let object = match self.require_data_view_borrowed(realm, &this_value)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let view = self.data_view_snapshot(object)?; if kind == DataViewNativeKind::Buffer { let buffer = ObjectRef::from_borrowed_handle(self.clone(), view.buffer)?; - return Ok(Completion::Return(Value::Object(buffer))); + return Ok(Completion::Return( + self.into_jsvalue(Value::Object(buffer))?, + )); } let buffer = self.snapshot_buffer_access(view.buffer)?.state; let Some(byte_length) = data_view_in_bounds_byte_length(view, buffer) else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached or resized", @@ -272,7 +279,7 @@ impl Runtime { )); } }; - Ok(Completion::Return(value)) + Ok(Completion::Return(self.into_jsvalue(value)?)) } fn call_data_view_get( @@ -752,7 +759,7 @@ fn data_view_decode(element: DataViewElementKind, bytes: [u8; 8], little_endian: pub(crate) enum DataViewAccessStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: DataViewAccessResume, }, } @@ -805,39 +812,34 @@ impl DataViewAccessStep { "DataView getter method received a constructor invocation" })); }; - let object = match runtime.require_data_view(realm, this_value.clone())? { + let object = match runtime.require_data_view(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; // Preserve the original payload check before the first coercion. runtime.data_view_snapshot(&object)?; - let value = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant(if set { + let value = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant(if set { "DataView set byteOffset argument was not padded" } else { "DataView get byteOffset argument was not padded" - }))? - .clone(); + }), + )?)?; let phase = if set { - AccessPhase::SetPosition( - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "DataView set value argument was not padded", - ))? - .clone(), - ) + AccessPhase::SetPosition(runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("DataView set value argument was not padded"), + )?)?) } else { AccessPhase::GetPosition }; - let endian = arguments - .readable - .get(if set { 2 } else { 1 }) - .cloned() - .unwrap_or(Value::Bool(false)); + let endian = match arguments.readable.get(if set { 2 } else { 1 }) { + Some(value) => runtime.root_value(value)?, + None => Value::Bool(false), + }; Ok(Self::Primitive { value, resume: DataViewAccessResume(Box::new(DataViewAccessResumeState { @@ -857,7 +859,7 @@ impl DataViewAccessResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(DataViewAccessStep::Complete(Completion::Throw(value))); } @@ -872,12 +874,14 @@ impl DataViewAccessResume { let position = match runtime.native_to_index(self.0.realm, &value)? { NativeConversion::Value(position) => position, NativeConversion::Throw(value) => { - return Ok(DataViewAccessStep::Complete(Completion::Throw(value))); + return Ok(DataViewAccessStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if let AccessPhase::SetPosition(value) = self.0.phase { return Ok(DataViewAccessStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: { let updated_0 = AccessPhase::SetValue(position); self.0.phase = updated_0; @@ -895,11 +899,13 @@ impl DataViewAccessResume { )? { NativeConversion::Value(bytes) => bytes, NativeConversion::Throw(value) => { - return Ok(DataViewAccessStep::Complete(Completion::Throw(value))); + return Ok(DataViewAccessStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(DataViewAccessStep::Complete(Completion::Return( - data_view_decode(self.0.element, bytes, little_endian), + runtime.into_jsvalue(data_view_decode(self.0.element, bytes, little_endian))?, ))) } AccessPhase::SetValue(position) => { @@ -910,7 +916,9 @@ impl DataViewAccessResume { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DataViewAccessStep::Complete(Completion::Throw(value))); + return Ok(DataViewAccessStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let little_endian = runtime.value_to_boolean(&self.0.endian)?; @@ -924,8 +932,10 @@ impl DataViewAccessResume { self.0.element, &bytes, )? { - NativeConversion::Value(()) => Completion::Return(Value::Undefined), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(()) => Completion::Return(JsValue::Undefined), + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) + } }, )) } @@ -941,8 +951,8 @@ fn finish_access( step = match step { DataViewAccessStep::Complete(result) => return Ok(result), DataViewAccessStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; @@ -955,11 +965,11 @@ fn finish_access( pub(crate) enum DataViewConstructorStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: DataViewConstructorResume, }, Prototype { - new_target: Value, + new_target: JsValue, resume: DataViewConstructorResume, }, } @@ -1000,46 +1010,38 @@ impl DataViewConstructorStep { "DataView constructor did not receive a constructor invocation", )); }; - let buffer = match runtime.require_data_view_array_buffer( - realm, - arguments.readable.first().ok_or(RuntimeError::Invariant( - "DataView buffer argument was not padded", - ))?, - )? { + let buffer_value = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("DataView buffer argument was not padded"), + )?)?; + let buffer = match runtime.require_data_view_array_buffer(realm, &buffer_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = if arguments.actual_arg_count > 2 - && !matches!(arguments.readable.get(2), Some(Value::Undefined)) + && !matches!(arguments.readable.get(2), Some(JsValue::Undefined)) { - Some( - arguments - .readable - .get(2) - .ok_or(RuntimeError::Invariant( - "DataView byteLength argument was not padded", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(2).ok_or( + RuntimeError::Invariant("DataView byteLength argument was not padded"), + )?)?) } else { None }; let resume = DataViewConstructorResume(Box::new(DataViewConstructorResumeState { realm, buffer, - new_target: new_target.clone(), + new_target: runtime.root_value(new_target)?, length, phase: DataViewConstructorPhase::Offset, })); if arguments.actual_arg_count > 1 { Ok(Self::Primitive { - value: arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "DataView byteOffset argument was not padded", - ))? - .clone(), + value: runtime.dup_jsvalue(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("DataView byteOffset argument was not padded"), + )?)?, resume, }) } else { @@ -1048,15 +1050,20 @@ impl DataViewConstructorStep { } } impl DataViewConstructorResume { - fn lookup(mut self, offset: u32, length: Option) -> DataViewConstructorStep { - DataViewConstructorStep::Prototype { - new_target: self.0.new_target.clone(), + fn lookup( + mut self, + runtime: &Runtime, + offset: u32, + length: Option, + ) -> Result { + Ok(DataViewConstructorStep::Prototype { + new_target: runtime.into_jsvalue(self.0.new_target.clone())?, resume: { let updated_0 = DataViewConstructorPhase::Prototype { offset, length }; self.0.phase = updated_0; self }, - } + }) } fn offset( mut self, @@ -1068,7 +1075,7 @@ impl DataViewConstructorResume { .state; if initial.detached { return Ok(DataViewConstructorStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "ArrayBuffer is detached", @@ -1077,7 +1084,7 @@ impl DataViewConstructorResume { } if offset > u64::from(initial.byte_length) { return Ok(DataViewConstructorStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid byteOffset", @@ -1088,7 +1095,7 @@ impl DataViewConstructorResume { .map_err(|_| RuntimeError::Invariant("validated DataView offset overflowed u32"))?; if let Some(length) = &self.0.length { Ok(DataViewConstructorStep::Primitive { - value: length.clone(), + value: runtime.into_jsvalue(length.clone())?, resume: { let updated_0 = DataViewConstructorPhase::Length { offset, @@ -1099,14 +1106,15 @@ impl DataViewConstructorResume { }, }) } else { - Ok(self.lookup( + self.lookup( + runtime, offset, if initial.max_byte_length.is_some() { None } else { Some(initial.byte_length - offset) }, - )) + ) } } pub(crate) fn resume( @@ -1115,7 +1123,7 @@ impl DataViewConstructorResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(DataViewConstructorStep::Complete(Completion::Throw(value))); } @@ -1128,7 +1136,9 @@ impl DataViewConstructorResume { let value = match runtime.native_to_index(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DataViewConstructorStep::Complete(Completion::Throw(value))); + return Ok(DataViewConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -1136,7 +1146,7 @@ impl DataViewConstructorResume { DataViewConstructorPhase::Length { offset, available } => { if value > u64::from(available) { return Ok(DataViewConstructorStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid byteLength", @@ -1146,7 +1156,7 @@ impl DataViewConstructorResume { let length = u32::try_from(value).map_err(|_| { RuntimeError::Invariant("validated DataView length overflowed u32") })?; - Ok(self.lookup(offset, Some(length))) + self.lookup(runtime, offset, Some(length)) } DataViewConstructorPhase::Prototype { .. } => Err(RuntimeError::Invariant( "DataView prototype request received an untyped reply", @@ -1164,7 +1174,9 @@ impl DataViewConstructorResume { runtime.data_view_default_prototype(realm)? } NativeConversion::Throw(value) => { - return Ok(DataViewConstructorStep::Complete(Completion::Throw(value))); + return Ok(DataViewConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let DataViewConstructorPhase::Prototype { offset, length } = self.0.phase else { @@ -1192,21 +1204,24 @@ fn finish_constructor( step = match step { DataViewConstructorStep::Complete(result) => return Ok(result), DataViewConstructorStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; resume.resume(runtime, result)? } - DataViewConstructorStep::Prototype { new_target, resume } => resume.prototype( - runtime, - finish_source( + DataViewConstructorStep::Prototype { new_target, resume } => { + let new_target = runtime.root_and_release_jsvalue(new_target)?; + resume.prototype( runtime, - realm, - ProtoSourceStep::start(runtime, realm, new_target)?, - )?, - )?, + finish_source( + runtime, + realm, + ProtoSourceStep::start(runtime, realm, new_target)?, + )?, + )? + } }; } } diff --git a/src/engine/builtins/array_buffer/data_view/tests.rs b/src/engine/builtins/array_buffer/data_view/tests.rs index 43c1e827..aae72b6a 100644 --- a/src/engine/builtins/array_buffer/data_view/tests.rs +++ b/src/engine/builtins/array_buffer/data_view/tests.rs @@ -580,10 +580,12 @@ fn pending_data_view_access_roots_the_view_and_buffer_until_abandonment() { }; let id = object.object_id(); let buffer = runtime.data_view_snapshot(object).unwrap().buffer; - let invocation = NativeInvocation::Call { this_value: view }; + let invocation = NativeInvocation::Call { + this_value: runtime.into_jsvalue(view).unwrap(), + }; let arguments = NativeArguments { actual_arg_count: 2, - readable: vec![Value::Int(0), Value::Int(42)], + readable: vec![JsValue::Int(0), JsValue::Int(42)], }; let DataViewAccessStep::Primitive { resume, .. } = DataViewAccessStep::start( &runtime, @@ -595,10 +597,17 @@ fn pending_data_view_access_roots_the_view_and_buffer_until_abandonment() { .unwrap() else { panic!("expected position conversion") }; - drop(invocation); - drop(arguments); + { + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + } let DataViewAccessStep::Primitive { resume, .. } = resume - .resume(&runtime, Completion::Return(Value::Int(0))) + .resume(&runtime, Completion::Return(JsValue::Int(0))) .unwrap() else { panic!("expected value conversion") diff --git a/src/engine/builtins/array_buffer/mutation.rs b/src/engine/builtins/array_buffer/mutation.rs index 92edf325..969c5062 100644 --- a/src/engine/builtins/array_buffer/mutation.rs +++ b/src/engine/builtins/array_buffer/mutation.rs @@ -4,7 +4,7 @@ use crate::engine::{ builtins::native::ArrayBufferNativeKind, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -13,7 +13,7 @@ use crate::engine::{ pub(crate) enum BufferMutationStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: BufferMutationResume, }, } @@ -48,18 +48,19 @@ impl BufferMutationStep { "SharedArrayBuffer.prototype.grow received a constructor invocation", )); }; - let object = match runtime.require_shared_array_buffer(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let object = + match runtime.require_shared_array_buffer(realm, runtime.root_value(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::Primitive { - value: arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "SharedArrayBuffer grow argument was not padded", - ))? - .clone(), + value: runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("SharedArrayBuffer grow argument was not padded"), + )?)?, resume: BufferMutationResume(Box::new(BufferMutationResumeState { realm, object, @@ -95,14 +96,18 @@ impl BufferMutationStep { }, )); }; - let object = match runtime.require_array_buffer(realm, this_value.clone())? { + let object = match runtime.require_array_buffer(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; if !matches!(kind, ArrayBufferNativeKind::Resize) { let initial = runtime.array_buffer_snapshot(&object)?; if arguments.actual_arg_count == 0 - || matches!(arguments.readable.first(), Some(Value::Undefined)) + || matches!(arguments.readable.first(), Some(JsValue::Undefined)) { return Ok(Self::Complete(runtime.finish_array_buffer_transfer( realm, @@ -112,17 +117,13 @@ impl BufferMutationStep { )?)); } } - let value = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - if matches!(kind, ArrayBufferNativeKind::Resize) { - "ArrayBuffer resize argument was not padded" - } else { - "ArrayBuffer transfer argument was not padded" - }, - ))? - .clone(); + let value = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant(if matches!(kind, ArrayBufferNativeKind::Resize) { + "ArrayBuffer resize argument was not padded" + } else { + "ArrayBuffer transfer argument was not padded" + }), + )?)?; Ok(Self::Primitive { value, resume: BufferMutationResume(Box::new(BufferMutationResumeState { @@ -141,7 +142,7 @@ impl BufferMutationResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(BufferMutationStep::Complete(Completion::Throw(value))); } @@ -164,7 +165,7 @@ impl BufferMutationResume { runtime.finish_array_buffer_resize(self.0.realm, self.0.object, length)? } } - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), } } else { match runtime.native_to_index(self.0.realm, &value)? { @@ -174,7 +175,7 @@ impl BufferMutationResume { length, matches!(self.0.kind, ArrayBufferNativeKind::TransferToFixedLength), )?, - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), } }; Ok(BufferMutationStep::Complete(result)) @@ -189,8 +190,8 @@ pub(in crate::engine::builtins) fn finish( step = match step { BufferMutationStep::Complete(result) => return Ok(result), BufferMutationStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/array_buffer/slice.rs b/src/engine/builtins/array_buffer/slice.rs index 95254ff0..e9f10783 100644 --- a/src/engine/builtins/array_buffer/slice.rs +++ b/src/engine/builtins/array_buffer/slice.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{ConstructorRef, NativeArguments, NativeInvocation}, @@ -17,7 +17,7 @@ pub(crate) enum BufferSliceKind { pub(crate) enum BufferSliceStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: BufferSliceResume, }, Read { @@ -27,7 +27,7 @@ pub(crate) enum BufferSliceStep { }, Construct { constructor: ConstructorRef, - arguments: Vec, + arguments: Vec, resume: BufferSliceResume, }, } @@ -71,41 +71,34 @@ impl BufferSliceStep { "Buffer slice received a constructor invocation", )); }; + let this_value = runtime.root_value(this_value)?; let source = match kind { - BufferSliceKind::Array => { - runtime.array_buffer_slice_source(realm, this_value.clone())? - } + BufferSliceKind::Array => runtime.array_buffer_slice_source(realm, this_value)?, BufferSliceKind::Shared => { - runtime.shared_array_buffer_slice_source(realm, this_value.clone())? + runtime.shared_array_buffer_slice_source(realm, this_value)? } }; let (source, length) = match source { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let end = if (matches!(kind, BufferSliceKind::Array) && arguments.actual_arg_count < 2) - || matches!(arguments.readable.get(1), Some(Value::Undefined)) + || matches!(arguments.readable.get(1), Some(JsValue::Undefined)) { None } else { - Some( - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "Buffer slice end argument was not padded", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("Buffer slice end argument was not padded"), + )?)?) }; Ok(Self::Primitive { - value: arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "Buffer slice start argument was not padded", - ))? - .clone(), + value: runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Buffer slice start argument was not padded"), + )?)?, resume: BufferSliceResume(Box::new(BufferSliceResumeState { realm, source, @@ -151,7 +144,9 @@ impl BufferSliceResume { let target = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(BufferSliceStep::Complete(Completion::Throw(value))); + return Ok(BufferSliceStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.copy(runtime, target, start, count) @@ -186,7 +181,7 @@ impl BufferSliceResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(BufferSliceStep::Complete(Completion::Throw(value))); } @@ -202,12 +197,14 @@ impl BufferSliceResume { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(BufferSliceStep::Complete(Completion::Throw(value))); + return Ok(BufferSliceStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if let Some(value) = end { Ok(BufferSliceStep::Primitive { - value: value.clone(), + value: runtime.into_jsvalue(value.clone())?, resume: { let updated_0 = Phase::End(start); self.0.phase = updated_0; @@ -229,7 +226,9 @@ impl BufferSliceResume { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(BufferSliceStep::Complete(Completion::Throw(value))); + return Ok(BufferSliceStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.select(runtime, start, end) @@ -240,7 +239,7 @@ impl BufferSliceResume { } let Value::Object(object) = value else { return Ok(BufferSliceStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", @@ -263,26 +262,30 @@ impl BufferSliceResume { } if !matches!(value, Value::Object(_)) { return Ok(BufferSliceStep::Complete(Completion::Throw( - runtime.new_not_constructor_error(self.0.realm, &value)?, + runtime.into_jsvalue( + runtime.new_not_constructor_error(self.0.realm, &value)?, + )?, ))); } let constructor = match runtime.constructor_from_value(self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(BufferSliceStep::Complete(Completion::Throw(value))); + return Ok(BufferSliceStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let mut arguments = Vec::new(); if arguments.try_reserve_exact(1).is_err() { return Ok(BufferSliceStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Internal, "out of memory", )?, ))); } - arguments.push(Value::Int( + arguments.push(JsValue::Int( i32::try_from(count).expect("Buffer slice length is bounded by i32::MAX"), )); Ok(BufferSliceStep::Construct { @@ -298,7 +301,7 @@ impl BufferSliceResume { Phase::Construct { start, count } => { let Value::Object(target) = value else { return Ok(BufferSliceStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, match self.0.kind { @@ -322,8 +325,8 @@ pub(in crate::engine::builtins) fn finish( step = match step { BufferSliceStep::Complete(result) => return Ok(result), BufferSliceStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; @@ -341,15 +344,21 @@ pub(in crate::engine::builtins) fn finish( constructor, arguments, resume, - } => resume.resume( - runtime, - runtime.construct_constructor_internal( - realm, - &constructor, - &constructor, - &arguments, - )?, - )?, + } => { + let arguments = arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; + resume.resume( + runtime, + runtime.construct_constructor_internal( + realm, + &constructor, + &constructor, + &arguments, + )?, + )? + } }; } } diff --git a/src/engine/builtins/array_buffer/typed_array.rs b/src/engine/builtins/array_buffer/typed_array.rs index 742793dc..6722d3e3 100644 --- a/src/engine/builtins/array_buffer/typed_array.rs +++ b/src/engine/builtins/array_buffer/typed_array.rs @@ -31,7 +31,7 @@ use crate::engine::{ AccessorValue, CallableRef, CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -432,8 +432,8 @@ impl Runtime { let to_string_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::ToString)?; let to_string = match self.get_property_in_realm(realm, &array_prototype, &to_string_key)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) | Completion::Throw(_) => { + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(_) => { return Err(RuntimeError::Invariant( "Array.prototype.toString was unavailable during TypedArray bootstrap", )); @@ -458,8 +458,8 @@ impl Runtime { let values_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Values)?; let values = match self.get_property_in_realm(realm, &base_prototype, &values_key)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) | Completion::Throw(_) => { + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(_) => { return Err(RuntimeError::Invariant( "TypedArray values was unavailable during iterator alias bootstrap", )); @@ -539,11 +539,9 @@ impl Runtime { arguments: &NativeArguments, ) -> Result { match kind { - TypedArrayNativeKind::BaseConstructor => Ok(Completion::Throw(self.new_native_error( - realm, - NativeErrorKind::Type, - "cannot be called", - )?)), + TypedArrayNativeKind::BaseConstructor => Ok(Completion::Throw( + self.new_native_error_jsvalue(realm, NativeErrorKind::Type, "cannot be called")?, + )), TypedArrayNativeKind::Constructor(element) => { self.call_typed_array_constructor(realm, element, invocation, arguments) } @@ -713,7 +711,7 @@ impl Runtime { "TypedArray species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } pub(in crate::engine::builtins) fn call_typed_array_getter( @@ -728,19 +726,23 @@ impl Runtime { )); }; if kind == TypedArrayNativeKind::ToStringTag { + let this_value = self.root_value(this_value)?; let Value::Object(object) = this_value else { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); }; - let Some(snapshot) = self.typed_array_snapshot_if_branded(object)? else { - return Ok(Completion::Return(Value::Undefined)); + let Some(snapshot) = self.typed_array_snapshot_if_branded(&object)? else { + return Ok(Completion::Return(JsValue::Undefined)); }; - return Ok(Completion::Return(Value::String(JsString::from_static( - snapshot.element.name(), - )))); + return Ok(Completion::Return(self.into_jsvalue(Value::String( + JsString::from_static(snapshot.element.name()), + ))?)); } - let object = match self.require_typed_array_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let object = match self.require_typed_array_borrowed(realm, &this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let state = self.typed_array_state(object)?; let result = match kind { @@ -761,7 +763,7 @@ impl Runtime { )); } }; - Ok(Completion::Return(result)) + Ok(Completion::Return(self.into_jsvalue(result)?)) } pub(in crate::engine::builtins) fn call_typed_array_iterator( @@ -775,17 +777,22 @@ impl Runtime { "TypedArray iterator factory received a constructor invocation", )); }; - let object = match self.require_typed_array_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let object = match self.require_typed_array_borrowed(realm, &this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; match self.typed_array_validated_length(realm, object)? { NativeConversion::Value(_) => {} - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } - Ok(Completion::Return(Value::Object( + Ok(Completion::Return(self.into_jsvalue(Value::Object( self.new_array_iterator(realm, object, kind)?, - ))) + ))?)) } fn call_typed_array_from( @@ -838,7 +845,7 @@ impl Runtime { ) -> Result { let source_state = self.typed_array_state_from_snapshot(source_snapshot)?; if source_state.out_of_bounds { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "out of bound", @@ -849,7 +856,7 @@ impl Runtime { .checked_add(u64::from(source_state.length)) .is_none_or(|end| end > u64::from(target_length)) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "out of bound", @@ -886,11 +893,13 @@ impl Runtime { .unwrap_or(Value::Undefined); match self.typed_array_set_index(realm, target, offset + index, &value)? { NativeConversion::Value(()) => {} - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } } } - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } fn require_typed_array( @@ -1046,7 +1055,7 @@ impl Runtime { collect::finish_collect( self, realm, - collect::TypedCollectStep::start(realm, source, method.clone(), element), + collect::TypedCollectStep::start(self, realm, source, method.clone(), element)?, ) } @@ -1320,7 +1329,8 @@ impl Runtime { element: TypedArrayElementKind, value: &Value, ) -> Result, RuntimeError> { - element::ElementStep::start(self, realm, element, value.clone())?.finish_sync(self, realm) + element::ElementStep::start(self, realm, element, self.unroot_value(value)?)? + .finish_sync(self, realm) } /// Convert a primitive descriptor value for the public context-free @@ -1428,7 +1438,7 @@ impl Runtime { heap: &mut crate::engine::heap::Heap, object: ObjectId, index: u32, - ) -> Option { + ) -> Option { let data = heap.object(object).ok()?; let snapshot = typed_array_snapshot_from_payload(&data.payload)?; if snapshot.element.is_bigint() { @@ -1439,23 +1449,27 @@ impl Runtime { else { return None; }; - Some(typed_array_decode(snapshot.element, bytes)) + Some(match typed_array_decode(snapshot.element, bytes) { + Value::Int(value) => JsValue::Int(value), + Value::Float(value) => JsValue::Float(value), + _ => unreachable!("typed array decode always yields a number"), + }) } /// Resident VM leaf: every decline precedes the only byte write. The /// owning input may be dropped after success without running heap cleanup. pub(crate) fn try_typed_array_number_write( &self, - base: &Value, + base: &JsValue, index: u32, number: f64, ) -> bool { use crate::engine::heap::SlotReleaseReadiness; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return false; }; if !matches!( - self.slot_value_release_readiness(base), + self.slot_value_release_readiness_jsvalue(base), Ok(SlotReleaseReadiness::Ready) ) { return false; @@ -1463,7 +1477,7 @@ impl Runtime { let Ok(mut state) = self.0.state.try_borrow_mut() else { return false; }; - let Ok(data) = state.heap.object(object.object_id()) else { + let Ok(data) = state.heap.object(*object) else { return false; }; let Some(snapshot) = typed_array_snapshot_from_payload(&data.payload) else { diff --git a/src/engine/builtins/array_buffer/typed_array/collect.rs b/src/engine/builtins/array_buffer/typed_array/collect.rs index 6e4f85ba..d5782d0e 100644 --- a/src/engine/builtins/array_buffer/typed_array/collect.rs +++ b/src/engine/builtins/array_buffer/typed_array/collect.rs @@ -4,14 +4,14 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::Completion, }; pub(crate) enum TypedIteratorMethodStep { Complete(NativeConversion>), Read { - receiver: Value, + receiver: JsValue, key: PropertyKey, resume: TypedIteratorMethodResume, }, @@ -31,7 +31,7 @@ impl TypedIteratorMethodStep { ))); } Ok(Self::Read { - receiver: source, + receiver: runtime.into_jsvalue(source)?, key: PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), resume: TypedIteratorMethodResume { realm }, }) @@ -47,19 +47,22 @@ impl TypedIteratorMethodResume { Completion::Return(value) => value, Completion::Throw(value) => { return Ok(TypedIteratorMethodStep::Complete(NativeConversion::Throw( - value, + runtime.root_and_release_jsvalue(value)?, ))); } }; - if matches!(value, Value::Null | Value::Undefined) { + if matches!(value, JsValue::Null | JsValue::Undefined) { return Ok(TypedIteratorMethodStep::Complete(NativeConversion::Value( None, ))); } - let callable = match value { - Value::Object(object) => runtime.as_callable(&object)?, + let callable = match &value { + JsValue::Object(id) => { + runtime.as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)? + } _ => None, }; + runtime.release_jsvalue(value)?; Ok(TypedIteratorMethodStep::Complete(match callable { Some(value) => NativeConversion::Value(Some(value)), None => NativeConversion::Throw(runtime.new_native_error( @@ -82,10 +85,13 @@ pub(crate) fn finish_method( receiver, key, resume, - } => resume.resume( - runtime, - runtime.get_value_property_in_realm(realm, receiver, &key)?, - )?, + } => { + let receiver = runtime.root_and_release_jsvalue(receiver)?; + resume.resume( + runtime, + runtime.get_value_property_in_realm(realm, receiver, &key)?, + )? + } }; } } @@ -94,7 +100,7 @@ pub(crate) enum TypedCollectStep { Complete(NativeConversion>), Call { callable: CallableRef, - receiver: Value, + receiver: JsValue, resume: TypedCollectResume, }, Read { @@ -137,14 +143,15 @@ pub(crate) struct TypedCollectResumeState { } impl TypedCollectStep { pub(crate) fn start( + runtime: &Runtime, realm: ContextId, source: Value, method: CallableRef, element: TypedArrayElementKind, - ) -> Self { - Self::Call { + ) -> Result { + Ok(Self::Call { callable: method.clone(), - receiver: source, + receiver: runtime.into_jsvalue(source)?, resume: TypedCollectResume(Box::new(TypedCollectResumeState { realm, _method: method, @@ -157,7 +164,7 @@ impl TypedCollectStep { values: Vec::new(), phase: Phase::Factory, })), - } + }) } } impl TypedCollectResume { @@ -180,14 +187,15 @@ impl TypedCollectResume { "TypedArray iterator lost cached next", ))? .clone(), - receiver: Value::Object( + receiver: JsValue::Object( self.0 .iterator .as_ref() .ok_or(RuntimeError::Invariant( "TypedArray collection lost iterator", ))? - .clone(), + .clone() + .into_handle(), ), resume: self, }) @@ -198,8 +206,10 @@ impl TypedCollectResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(self.abrupt(value)), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(self.abrupt(runtime.root_and_release_jsvalue(value)?)); + } }; match self.0.phase { Phase::Factory => { @@ -307,10 +317,13 @@ pub(crate) fn finish_collect( callable, receiver, resume, - } => resume.resume( - runtime, - runtime.call_internal(realm, &callable, receiver, &[])?, - )?, + } => { + let receiver = runtime.root_and_release_jsvalue(receiver)?; + resume.resume( + runtime, + runtime.call_internal(realm, &callable, receiver, &[])?, + )? + } TypedCollectStep::Read { object, key, diff --git a/src/engine/builtins/array_buffer/typed_array/copying.rs b/src/engine/builtins/array_buffer/typed_array/copying.rs index 95683adb..b65303eb 100644 --- a/src/engine/builtins/array_buffer/typed_array/copying.rs +++ b/src/engine/builtins/array_buffer/typed_array/copying.rs @@ -11,7 +11,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -45,7 +45,7 @@ impl Runtime { ) -> Result { let current = self.typed_array_state(&source)?; if current.out_of_bounds || index < 0 || index >= i64::from(current.length) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid array index", @@ -58,15 +58,21 @@ impl Runtime { let target = match self.typed_array_copy_to_default(realm, &source, element, initial_length)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let index = u64::try_from(index) .map_err(|_| RuntimeError::Invariant("validated TypedArray.with index was negative"))?; match self.typed_array_set_index(realm, &target, index, &replacement)? { NativeConversion::Value(()) => {} - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target))?, + )) } pub(crate) fn call_typed_array_to_reversed( @@ -79,9 +85,12 @@ impl Runtime { "TypedArray.prototype.toReversed received a constructor invocation", )); }; - let source = match self.require_typed_array_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let source = match self.require_typed_array_borrowed(realm, &this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let state = self.typed_array_state(source)?; let target = match self.typed_array_copy_to_default( @@ -91,7 +100,9 @@ impl Runtime { u64::from(state.length), )? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let target_state = self.typed_array_state(&target)?; if target_state.length > 1 { @@ -113,7 +124,9 @@ impl Runtime { } })?; } - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target))?, + )) } /// QuickJS's internal same-class TypedArray constructor used by copying @@ -210,7 +223,7 @@ impl Runtime { pub(crate) enum TypedWithStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: TypedWithResume, }, } @@ -250,35 +263,31 @@ impl TypedWithStep { "TypedArray.prototype.with received a constructor invocation", )); }; - let source = match runtime.require_typed_array(realm, this_value.clone())? { + let source = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let initial = runtime.typed_array_state(&source)?; if initial.out_of_bounds { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", )?, ))); } - let replacement = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray.with replacement argv was not padded", - ))? - .clone(); + let replacement = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("TypedArray.with replacement argv was not padded"), + )?)?; Ok(Self::Primitive { - value: arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray.with index argv was not padded", - ))? - .clone(), + value: runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray.with index argv was not padded"), + )?)?, resume: TypedWithResume(Box::new(TypedWithResumeState { realm, source, @@ -296,7 +305,7 @@ impl TypedWithResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedWithStep::Complete(Completion::Throw(value))); } @@ -306,7 +315,9 @@ impl TypedWithResume { let index = match runtime.native_to_int64_sat(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedWithStep::Complete(Completion::Throw(value))); + return Ok(TypedWithStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let index = if index < 0 { @@ -315,7 +326,7 @@ impl TypedWithResume { index }; Ok(TypedWithStep::Primitive { - value: replacement, + value: runtime.into_jsvalue(replacement)?, resume: { let updated_0 = WithPhase::Replacement(index); self.0.phase = updated_0; @@ -345,8 +356,8 @@ fn finish( step = match step { TypedWithStep::Complete(result) => return Ok(result), TypedWithStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/array_buffer/typed_array/create.rs b/src/engine/builtins/array_buffer/typed_array/create.rs index e24c5ada..a0d05b58 100644 --- a/src/engine/builtins/array_buffer/typed_array/create.rs +++ b/src/engine/builtins/array_buffer/typed_array/create.rs @@ -4,7 +4,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{ @@ -131,14 +131,11 @@ impl TypedCreateStep { "TypedArray constructor did not receive a constructor invocation", )); }; - let first = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray constructor argv was not padded", - ))? - .clone(); - let purpose = if let Value::Object(source) = first { + let first = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray constructor argv was not padded"), + )?)?; + let purpose = if let Value::Object(source) = &first { + let source = source.clone(); if !source.belongs_to(runtime) { return Err(RuntimeError::WrongRuntime("TypedArray constructor source")); } @@ -147,30 +144,18 @@ impl TypedCreateStep { .is_some() { let offset = if arguments.actual_arg_count > 1 { - Some( - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray byteOffset argv was not padded", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("TypedArray byteOffset argv was not padded"), + )?)?) } else { None }; let length = if arguments.actual_arg_count > 2 - && !matches!(arguments.readable.get(2), Some(Value::Undefined)) + && !matches!(arguments.readable.get(2), Some(JsValue::Undefined)) { - Some( - arguments - .readable - .get(2) - .ok_or(RuntimeError::Invariant( - "TypedArray length argv was not padded", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(2).ok_or( + RuntimeError::Invariant("TypedArray length argv was not padded"), + )?)?) } else { None }; @@ -189,15 +174,17 @@ impl TypedCreateStep { let length = match runtime.native_to_index(realm, &first)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; ProtoPurpose::Length(length) }; Ok(Self::request_prototype( - new_target.clone(), + runtime.dup_jsvalue(new_target)?, TypedCreateResume(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm, phase: Phase::Prototype { element, purpose }, })), @@ -214,23 +201,24 @@ impl TypedCreateStep { "TypedArray.from received a constructor invocation", )); }; - let source = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray.from argv was not padded", - ))? - .clone(); + let source = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray.from argv was not padded"), + )?)?; let mapper = if arguments.actual_arg_count > 1 - && !matches!(arguments.readable[1], Value::Undefined) + && !matches!(arguments.readable[1], JsValue::Undefined) { - let callable = match &arguments.readable[1] { + let mapper_value = runtime.root_value(&arguments.readable[1])?; + let callable = match &mapper_value { Value::Object(object) => runtime.as_callable(object)?, _ => None, }; let Some(callable) = callable else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; Some(callable) @@ -238,7 +226,7 @@ impl TypedCreateStep { None }; let this_arg = if arguments.actual_arg_count > 2 { - arguments.readable[2].clone() + runtime.root_value(&arguments.readable[2])? } else { Value::Undefined }; @@ -249,7 +237,7 @@ impl TypedCreateStep { }; if let Some(message) = message { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, message)?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, message)?, ))); } TypedCreateResume::iterator( @@ -257,7 +245,7 @@ impl TypedCreateStep { realm, source, Factory { - allocation: Allocation::Static(this_value.clone()), + allocation: Allocation::Static(runtime.root_value(this_value)?), mapper, this_arg, }, @@ -286,14 +274,15 @@ impl TypedCreateStep { values.extend( arguments.readable[..arguments.actual_arg_count] .iter() - .cloned(), + .map(|value| runtime.root_value(value)) + .collect::, _>>()?, ); TypedCreateResume::allocate( runtime, realm, Input::Values(values), Factory { - allocation: Allocation::Static(this_value.clone()), + allocation: Allocation::Static(runtime.root_value(this_value)?), mapper: None, this_arg: Value::Undefined, }, @@ -303,15 +292,15 @@ impl TypedCreateStep { } impl TypedCreateResume { fn iterator( - _runtime: &Runtime, + runtime: &Runtime, realm: ContextId, source: Value, factory: Factory, ) -> Result { Ok(TypedCreateStep::request_method( - source.clone(), + runtime.into_jsvalue(source.clone())?, Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm, phase: Phase::Iterator { source, factory }, })), @@ -325,7 +314,9 @@ impl TypedCreateResume { let method = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Iterator { source, factory } = self.0.phase else { @@ -339,11 +330,11 @@ impl TypedCreateResume { Allocation::Static(_) => TypedArrayElementKind::Uint8, }; Ok(TypedCreateStep::request_collect( - source, + runtime.into_jsvalue(source)?, method, element, Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Collect(factory), })), @@ -352,14 +343,16 @@ impl TypedCreateResume { let source = match runtime.native_to_object(self.0.realm, source)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(TypedCreateStep::request_read( - Value::Object(source.clone()), + JsValue::Object(source.clone().into_handle()), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Length { source, factory }, })), @@ -383,26 +376,29 @@ impl TypedCreateResume { runtime.typed_array_default_prototype(realm, element)? } NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match purpose { - ProtoPurpose::Length(length) => complete_object(runtime.new_typed_array_for_length( - self.0.realm, - &prototype, - element, - length, - )?), + ProtoPurpose::Length(length) => complete_object( + runtime, + runtime.new_typed_array_for_length(self.0.realm, &prototype, element, length)?, + ), ProtoPurpose::Typed { source, length } => { let snapshot = runtime.typed_array_snapshot(&source)?; - complete_object(runtime.typed_array_copy_into_new( - self.0.realm, - &prototype, - element, - &source, - snapshot, - length, - )?) + complete_object( + runtime, + runtime.typed_array_copy_into_new( + self.0.realm, + &prototype, + element, + &source, + snapshot, + length, + )?, + ) } ProtoPurpose::Object(source) => Self::iterator( runtime, @@ -420,7 +416,7 @@ impl TypedCreateResume { length, } => { let resume = Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Offset { prototype, @@ -430,9 +426,12 @@ impl TypedCreateResume { }, })); if let Some(value) = offset { - Ok(TypedCreateStep::request_primitive(value, resume)) + Ok(TypedCreateStep::request_primitive( + runtime.into_jsvalue(value)?, + resume, + )) } else { - resume.resume(runtime, Completion::Return(Value::Int(0))) + resume.resume(runtime, Completion::Return(JsValue::Int(0))) } } } @@ -445,7 +444,9 @@ impl TypedCreateResume { let values = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Collect(factory) = self.0.phase else { @@ -476,7 +477,9 @@ impl TypedCreateResume { match runtime.new_typed_array_for_length(realm, prototype, *element, length)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Population { @@ -492,14 +495,18 @@ impl TypedCreateResume { Allocation::Static(constructor) => { if !matches!(constructor, Value::Object(_)) { return Ok(TypedCreateStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); } Ok(TypedCreateStep::request_create( - constructor.clone(), + runtime.into_jsvalue(constructor.clone())?, length, Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm, phase: Phase::Create { source, @@ -519,7 +526,9 @@ impl TypedCreateResume { let target = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Create { @@ -550,7 +559,9 @@ impl TypedCreateResume { let bytes = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Element(mut population) = self.0.phase else { @@ -568,7 +579,7 @@ impl TypedCreateResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedCreateStep::Complete(Completion::Throw(value))); } @@ -583,12 +594,14 @@ impl TypedCreateResume { let offset = match runtime.native_to_index(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if offset % u64::from(element.byte_length()) != 0 { return Ok(TypedCreateStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid offset", @@ -597,9 +610,9 @@ impl TypedCreateResume { } if let Some(value) = length { Ok(TypedCreateStep::request_primitive( - value, + runtime.into_jsvalue(value)?, Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::BufferLength { prototype, @@ -610,14 +623,17 @@ impl TypedCreateResume { })), )) } else { - complete_object(runtime.new_typed_array_constructor_view_from_coerced( - self.0.realm, - &prototype, - element, - &source, - offset, - None, - )?) + complete_object( + runtime, + runtime.new_typed_array_constructor_view_from_coerced( + self.0.realm, + &prototype, + element, + &source, + offset, + None, + )?, + ) } } Phase::BufferLength { @@ -629,22 +645,27 @@ impl TypedCreateResume { let length = match runtime.native_to_index(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; - complete_object(runtime.new_typed_array_constructor_view_from_coerced( - self.0.realm, - &prototype, - element, - &source, - offset, - Some(length), - )?) + complete_object( + runtime, + runtime.new_typed_array_constructor_view_from_coerced( + self.0.realm, + &prototype, + element, + &source, + offset, + Some(length), + )?, + ) } Phase::Length { source, factory } => Ok(TypedCreateStep::request_primitive( - value, + runtime.into_jsvalue(value)?, Self(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::LengthPrimitive { source, factory }, })), @@ -653,7 +674,9 @@ impl TypedCreateResume { let length = match runtime.native_to_length(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedCreateStep::Complete(Completion::Throw(value))); + return Ok(TypedCreateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Self::allocate( @@ -676,15 +699,15 @@ impl Population { fn next(self, runtime: &Runtime, realm: ContextId) -> Result { if self.index == self.length { return Ok(TypedCreateStep::Complete(Completion::Return( - Value::Object(self.target), + runtime.into_jsvalue(Value::Object(self.target))?, ))); } match &self.source { Input::Object(source) => Ok(TypedCreateStep::request_read( - Value::Object(source.clone()), + JsValue::Object(source.clone().into_handle()), runtime.property_key_for_index(self.index)?, TypedCreateResume(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm, phase: Phase::Index(self), })), @@ -713,10 +736,13 @@ impl Population { arguments.push(Value::number(self.index as f64)); Ok(TypedCreateStep::request_call( DirectCallTarget::Callable(mapper.clone()), - self.this_arg.clone(), - arguments, + runtime.into_jsvalue(self.this_arg.clone())?, + arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, TypedCreateResume(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm, phase: Phase::Mapped(self), })), @@ -734,24 +760,29 @@ impl Population { let element = runtime.typed_array_snapshot(&self.target)?.element; Ok(TypedCreateStep::request_element( element, - value, + runtime.into_jsvalue(value)?, TypedCreateResume(Box::new(TypedCreateResumeState { - pending_effect: TypedCreateStepPending::default(), + pending_effect: TypedCreateStepPending::new(runtime.clone()), realm, phase: Phase::Element(self), })), )) } } -fn complete_object(result: NativeConversion) -> Result { +fn complete_object( + runtime: &Runtime, + result: NativeConversion, +) -> Result { Ok(TypedCreateStep::Complete(match result { - NativeConversion::Value(value) => Completion::Return(Value::Object(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => { + Completion::Return(runtime.into_jsvalue(Value::Object(value))?) + } + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), })) } fn out_of_memory(runtime: &Runtime, realm: ContextId) -> Result { Ok(TypedCreateStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Internal, "out of memory")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Internal, "out of memory")?, ))) } pub(super) fn finish( @@ -765,8 +796,8 @@ pub(super) fn finish( TypedCreateStep::Primitive { mut resume } => { let value = resume.take_primitive_value(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; @@ -774,7 +805,8 @@ pub(super) fn finish( } } TypedCreateStep::Prototype { mut resume } => { - let new_target = resume.take_prototype_new_target(); + let new_target = + runtime.root_and_release_jsvalue(resume.take_prototype_new_target())?; resume.prototype( runtime, finish_source( @@ -785,7 +817,7 @@ pub(super) fn finish( )? } TypedCreateStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -793,11 +825,11 @@ pub(super) fn finish( )? } TypedCreateStep::Method { mut resume } => { - let source = resume.take_method_source(); + let source = runtime.root_and_release_jsvalue(resume.take_method_source())?; resume.method(runtime, runtime.typed_array_iterator_method(realm, source)?)? } TypedCreateStep::Collect { mut resume } => { - let source = resume.take_collect_source(); + let source = runtime.root_and_release_jsvalue(resume.take_collect_source())?; let method = resume.take_collect_method(); let element = resume.take_collect_element(); resume.collected( @@ -806,7 +838,8 @@ pub(super) fn finish( )? } TypedCreateStep::Create { mut resume } => { - let constructor = resume.take_create_constructor(); + let constructor = + runtime.root_and_release_jsvalue(resume.take_create_constructor())?; let length = resume.take_create_length(); resume.created( runtime, @@ -819,8 +852,12 @@ pub(super) fn finish( } TypedCreateStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -835,7 +872,7 @@ pub(super) fn finish( } TypedCreateStep::Element { mut resume } => { let element = resume.take_element_element(); - let value = resume.take_element_value(); + let value = runtime.root_and_release_jsvalue(resume.take_element_value())?; resume.element( runtime, runtime.typed_array_convert_element(realm, element, &value)?, @@ -876,7 +913,9 @@ mod tests { panic!("expected first conversion"); }; let _ = resume.take_element_element(); - drop(resume.take_element_value()); + runtime + .release_jsvalue(resume.take_element_value()) + .unwrap(); let step = resume .element(&runtime, NativeConversion::Value([0; 8])) .unwrap(); @@ -896,35 +935,84 @@ mod tests { } } -#[derive(Default)] struct TypedCreateStepPending { - primitive_value: Option, - prototype_new_target: Option, - read_receiver: Option, + runtime: Runtime, + primitive_value: Option, + prototype_new_target: Option, + read_receiver: Option, read_key: Option, - method_source: Option, - collect_source: Option, + method_source: Option, + collect_source: Option, collect_method: Option, collect_element: Option, - create_constructor: Option, + create_constructor: Option, create_length: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, element_element: Option, - element_value: Option, + element_value: Option, +} +impl TypedCreateStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + primitive_value: None, + prototype_new_target: None, + read_receiver: None, + read_key: None, + method_source: None, + collect_source: None, + collect_method: None, + collect_element: None, + create_constructor: None, + create_length: None, + call_target: None, + call_receiver: None, + call_arguments: None, + element_element: None, + element_value: None, + } + } +} +impl Drop for TypedCreateStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow. + fn drop(&mut self) { + for value in [ + self.primitive_value.take(), + self.prototype_new_target.take(), + self.read_receiver.take(), + self.method_source.take(), + self.collect_source.take(), + self.create_constructor.take(), + self.call_receiver.take(), + self.element_value.take(), + ] + .into_iter() + .flatten() + { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } } impl TypedCreateStep { - pub(crate) fn request_primitive(value: Value, mut resume: TypedCreateResume) -> Self { + pub(crate) fn request_primitive(value: JsValue, mut resume: TypedCreateResume) -> Self { resume.0.pending_effect.primitive_value = Some(value); Self::Primitive { resume } } - pub(crate) fn request_prototype(new_target: Value, mut resume: TypedCreateResume) -> Self { + pub(crate) fn request_prototype(new_target: JsValue, mut resume: TypedCreateResume) -> Self { resume.0.pending_effect.prototype_new_target = Some(new_target); Self::Prototype { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: TypedCreateResume, ) -> Self { @@ -932,12 +1020,12 @@ impl TypedCreateStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_method(source: Value, mut resume: TypedCreateResume) -> Self { + pub(crate) fn request_method(source: JsValue, mut resume: TypedCreateResume) -> Self { resume.0.pending_effect.method_source = Some(source); Self::Method { resume } } pub(crate) fn request_collect( - source: Value, + source: JsValue, method: CallableRef, element: TypedArrayElementKind, mut resume: TypedCreateResume, @@ -948,7 +1036,7 @@ impl TypedCreateStep { Self::Collect { resume } } pub(crate) fn request_create( - constructor: Value, + constructor: JsValue, length: u64, mut resume: TypedCreateResume, ) -> Self { @@ -958,8 +1046,8 @@ impl TypedCreateStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: TypedCreateResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -969,7 +1057,7 @@ impl TypedCreateStep { } pub(crate) fn request_element( element: TypedArrayElementKind, - value: Value, + value: JsValue, mut resume: TypedCreateResume, ) -> Self { resume.0.pending_effect.element_element = Some(element); @@ -978,21 +1066,21 @@ impl TypedCreateStep { } } impl TypedCreateResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .pending_effect .primitive_value .take() .expect("TypedCreateStep Primitive value") } - pub(crate) fn take_prototype_new_target(&mut self) -> Value { + pub(crate) fn take_prototype_new_target(&mut self) -> JsValue { self.0 .pending_effect .prototype_new_target .take() .expect("TypedCreateStep Prototype new_target") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -1006,14 +1094,14 @@ impl TypedCreateResume { .take() .expect("TypedCreateStep Read key") } - pub(crate) fn take_method_source(&mut self) -> Value { + pub(crate) fn take_method_source(&mut self) -> JsValue { self.0 .pending_effect .method_source .take() .expect("TypedCreateStep Method source") } - pub(crate) fn take_collect_source(&mut self) -> Value { + pub(crate) fn take_collect_source(&mut self) -> JsValue { self.0 .pending_effect .collect_source @@ -1034,7 +1122,7 @@ impl TypedCreateResume { .take() .expect("TypedCreateStep Collect element") } - pub(crate) fn take_create_constructor(&mut self) -> Value { + pub(crate) fn take_create_constructor(&mut self) -> JsValue { self.0 .pending_effect .create_constructor @@ -1055,14 +1143,14 @@ impl TypedCreateResume { .take() .expect("TypedCreateStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("TypedCreateStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -1076,7 +1164,7 @@ impl TypedCreateResume { .take() .expect("TypedCreateStep Element element") } - pub(crate) fn take_element_value(&mut self) -> Value { + pub(crate) fn take_element_value(&mut self) -> JsValue { self.0 .pending_effect .element_value diff --git a/src/engine/builtins/array_buffer/typed_array/element.rs b/src/engine/builtins/array_buffer/typed_array/element.rs index a27099c8..606daf24 100644 --- a/src/engine/builtins/array_buffer/typed_array/element.rs +++ b/src/engine/builtins/array_buffer/typed_array/element.rs @@ -8,7 +8,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion}, vm::Completion, }; @@ -41,7 +41,7 @@ impl ElementStep { runtime: &Runtime, realm: ContextId, element: TypedArrayElementKind, - value: Value, + value: JsValue, ) -> Result { from_primitive( runtime, @@ -68,8 +68,12 @@ impl ElementStep { } Self::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -84,8 +88,9 @@ pub(super) fn encode_primitive( runtime: &Runtime, realm: ContextId, element: TypedArrayElementKind, - value: Value, + value: JsValue, ) -> Result, RuntimeError> { + let value = runtime.root_and_release_jsvalue(value)?; Ok(if element.is_bigint() { match runtime.bigint_from_primitive(realm, value)? { NativeConversion::Value(bigint) => { @@ -109,9 +114,9 @@ fn from_primitive( step: PrimitiveStep, ) -> Result { Ok(match step { - PrimitiveStep::Complete(Completion::Throw(value)) => { - ElementStep::Complete(NativeConversion::Throw(value)) - } + PrimitiveStep::Complete(Completion::Throw(value)) => ElementStep::Complete( + NativeConversion::Throw(runtime.root_and_release_jsvalue(value)?), + ), PrimitiveStep::Complete(Completion::Return(value)) => { let bytes = encode_primitive(runtime, realm, element, value)?; ElementStep::Complete(bytes) @@ -167,8 +172,8 @@ struct ElementStepPending { read_object: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, } impl ElementStep { pub(crate) fn request_read( @@ -182,8 +187,8 @@ impl ElementStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ElementResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -214,14 +219,14 @@ impl ElementResume { .take() .expect("ElementStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ElementStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/array_buffer/typed_array/iteration.rs b/src/engine/builtins/array_buffer/typed_array/iteration.rs index 32f3c974..e6539349 100644 --- a/src/engine/builtins/array_buffer/typed_array/iteration.rs +++ b/src/engine/builtins/array_buffer/typed_array/iteration.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::{ArrayIterationKind, TypedArrayElementKind}, heap::ContextId, object::{CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{DirectCallTarget, NativeArguments, NativeInvocation}, @@ -92,31 +92,31 @@ impl TypedIterationStep { "TypedArray.prototype iteration received a constructor invocation", )); }; - let target = match runtime.require_typed_array(realm, this_value.clone())? { + let target = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = match runtime.typed_array_validated_length(realm, &target)? { NativeConversion::Value(value) => u64::from(value), - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; - let callback = runtime.callable_from_value( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray iteration callback argv was not padded", - ))? - .clone(), - )?; + let callback = runtime.callable_from_value(runtime.root_value( + arguments.readable.first().ok_or(RuntimeError::Invariant( + "TypedArray iteration callback argv was not padded", + ))?, + )?)?; let this_arg = if arguments.actual_arg_count > 1 { - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray iteration thisArg was missing", - ))? - .clone() + runtime.root_value(arguments.readable.get(1).ok_or(RuntimeError::Invariant( + "TypedArray iteration thisArg was missing", + ))?)? } else { Value::Undefined }; @@ -138,7 +138,7 @@ impl TypedIterationStep { element, length, TypedIterationResume(Box::new(TypedIterationResumeState { - pending_effect: TypedIterationStepPending::default(), + pending_effect: TypedIterationStepPending::new(runtime.clone()), realm, phase: IterationPhase::MapSpecies(input), })), @@ -167,25 +167,26 @@ impl TypedIterationResume { mut state: IterationState, ) -> Result { if state.index == state.input.length { + let result = match state.mode { + IterationMode::Every => Value::Bool(true), + IterationMode::Some => Value::Bool(false), + IterationMode::ForEach => Value::Undefined, + IterationMode::Map(target) => Value::Object(target), + IterationMode::Filter { selected, length } => { + return Ok(TypedIterationStep::request_species( + state.input.target, + state.input.element, + length, + Self(Box::new(TypedIterationResumeState { + pending_effect: TypedIterationStepPending::new(runtime.clone()), + realm, + phase: IterationPhase::FilterSpecies(selected), + })), + )); + } + }; return Ok(TypedIterationStep::Complete(Completion::Return( - match state.mode { - IterationMode::Every => Value::Bool(true), - IterationMode::Some => Value::Bool(false), - IterationMode::ForEach => Value::Undefined, - IterationMode::Map(target) => Value::Object(target), - IterationMode::Filter { selected, length } => { - return Ok(TypedIterationStep::request_species( - state.input.target, - state.input.element, - length, - Self(Box::new(TypedIterationResumeState { - pending_effect: TypedIterationStepPending::default(), - realm, - phase: IterationPhase::FilterSpecies(selected), - })), - )); - } - }, + runtime.into_jsvalue(result)?, ))); } let index = state.index; @@ -202,10 +203,13 @@ impl TypedIterationResume { arguments.push(Value::Object(state.input.target.clone())); Ok(TypedIterationStep::request_call( DirectCallTarget::Callable(state.input.callback.clone()), - state.input.this_arg.clone(), - arguments, + runtime.into_jsvalue(state.input.this_arg.clone())?, + arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, Self(Box::new(TypedIterationResumeState { - pending_effect: TypedIterationStepPending::default(), + pending_effect: TypedIterationStepPending::new(runtime.clone()), realm, phase: IterationPhase::Called { state, @@ -223,7 +227,9 @@ impl TypedIterationResume { let target = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedIterationStep::Complete(Completion::Throw(value))); + return Ok(TypedIterationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -240,7 +246,7 @@ impl TypedIterationResume { target.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Set)?, Self(Box::new(TypedIterationResumeState { - pending_effect: TypedIterationStepPending::default(), + pending_effect: TypedIterationStepPending::new(runtime.clone()), realm: self.0.realm, phase: IterationPhase::FilterMethod { target, selected }, })), @@ -258,7 +264,9 @@ impl TypedIterationResume { let bytes = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedIterationStep::Complete(Completion::Throw(value))); + return Ok(TypedIterationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let IterationPhase::Mapped { state, index } = self.0.phase else { @@ -279,7 +287,7 @@ impl TypedIterationResume { result: Completion, ) -> Result { let result = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedIterationStep::Complete(Completion::Throw(value))); } @@ -293,21 +301,21 @@ impl TypedIterationResume { match &mut state.mode { IterationMode::Every if !runtime.value_to_boolean(&result)? => { return Ok(TypedIterationStep::Complete(Completion::Return( - Value::Bool(false), + JsValue::Bool(false), ))); } IterationMode::Some if runtime.value_to_boolean(&result)? => { return Ok(TypedIterationStep::Complete(Completion::Return( - Value::Bool(true), + JsValue::Bool(true), ))); } IterationMode::Every | IterationMode::Some | IterationMode::ForEach => {} IterationMode::Map(target) => { return Ok(TypedIterationStep::request_element( runtime.typed_array_snapshot(target)?.element, - result, + runtime.into_jsvalue(result)?, Self(Box::new(TypedIterationResumeState { - pending_effect: TypedIterationStepPending::default(), + pending_effect: TypedIterationStepPending::new(runtime.clone()), realm: self.0.realm, phase: IterationPhase::Mapped { state, index }, })), @@ -348,17 +356,20 @@ impl TypedIterationResume { arguments.push(Value::Object(selected)); Ok(TypedIterationStep::request_call( DirectCallTarget::Callable(callable), - Value::Object(target.clone()), - arguments, + runtime.into_jsvalue(Value::Object(target.clone()))?, + arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, Self(Box::new(TypedIterationResumeState { - pending_effect: TypedIterationStepPending::default(), + pending_effect: TypedIterationStepPending::new(runtime.clone()), realm: self.0.realm, phase: IterationPhase::FilterCalled(target), })), )) } IterationPhase::FilterCalled(target) => Ok(TypedIterationStep::Complete( - Completion::Return(Value::Object(target)), + Completion::Return(runtime.into_jsvalue(Value::Object(target))?), )), _ => Err(RuntimeError::Invariant( "TypedArray iteration received an untyped reply", @@ -368,7 +379,7 @@ impl TypedIterationResume { } fn iteration_oom(runtime: &Runtime, realm: ContextId) -> Result { Ok(TypedIterationStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Internal, "out of memory")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Internal, "out of memory")?, ))) } impl Runtime { @@ -394,8 +405,12 @@ impl Runtime { } TypedIterationStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -427,19 +442,54 @@ impl Runtime { } } -#[derive(Default)] struct TypedIterationStepPending { + runtime: Runtime, species_source: Option, species_element: Option, species_length: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, element_element: Option, - element_value: Option, + element_value: Option, read_object: Option, read_key: Option, } +impl TypedIterationStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + species_source: None, + species_element: None, + species_length: None, + call_target: None, + call_receiver: None, + call_arguments: None, + element_element: None, + element_value: None, + read_object: None, + read_key: None, + } + } +} +impl Drop for TypedIterationStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow. + fn drop(&mut self) { + for value in [self.call_receiver.take(), self.element_value.take()] + .into_iter() + .flatten() + { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} impl TypedIterationStep { pub(crate) fn request_species( source: ObjectRef, @@ -454,8 +504,8 @@ impl TypedIterationStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: TypedIterationResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -465,7 +515,7 @@ impl TypedIterationStep { } pub(crate) fn request_element( element: TypedArrayElementKind, - value: Value, + value: JsValue, mut resume: TypedIterationResume, ) -> Self { resume.0.pending_effect.element_element = Some(element); @@ -511,14 +561,14 @@ impl TypedIterationResume { .take() .expect("TypedIterationStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("TypedIterationStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -532,7 +582,7 @@ impl TypedIterationResume { .take() .expect("TypedIterationStep Element element") } - pub(crate) fn take_element_value(&mut self) -> Value { + pub(crate) fn take_element_value(&mut self) -> JsValue { self.0 .pending_effect .element_value diff --git a/src/engine/builtins/array_buffer/typed_array/iteration/transform_tests.rs b/src/engine/builtins/array_buffer/typed_array/iteration/transform_tests.rs index 0b7d0335..0b2b9df6 100644 --- a/src/engine/builtins/array_buffer/typed_array/iteration/transform_tests.rs +++ b/src/engine/builtins/array_buffer/typed_array/iteration/transform_tests.rs @@ -639,9 +639,11 @@ fn pending_map_element_owns_source_target_callback_and_conversion_input() { }); let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![callback], + readable: vec![runtime.into_jsvalue(callback).unwrap()], + }; + let invocation = NativeInvocation::Call { + this_value: runtime.into_jsvalue(source).unwrap(), }; - let invocation = NativeInvocation::Call { this_value: source }; let TypedIterationStep::Species { mut resume } = TypedIterationStep::start( &runtime, context.realm, @@ -655,8 +657,15 @@ fn pending_map_element_owns_source_target_callback_and_conversion_input() { drop(resume.take_species_source()); let _ = resume.take_species_element(); let _ = resume.take_species_length(); - drop(invocation); - drop(arguments); + { + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + } let Value::Object(mapped) = mapped else { unreachable!() }; @@ -667,12 +676,19 @@ fn pending_map_element_owns_source_target_callback_and_conversion_input() { panic!("expected callback") }; drop(resume.take_call_target()); - drop(resume.take_call_receiver()); - drop(resume.take_call_arguments()); + runtime + .release_jsvalue(resume.take_call_receiver()) + .unwrap(); + for value in resume.take_call_arguments() { + runtime.release_jsvalue(value).unwrap(); + } let conversion = runtime.new_object(None).unwrap(); let conversion_id = conversion.object_id(); let step = resume - .resume(&runtime, Completion::Return(Value::Object(conversion))) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(Value::Object(conversion)).unwrap()), + ) .unwrap(); assert!(matches!(step, TypedIterationStep::Element { .. })); runtime.run_gc().unwrap(); diff --git a/src/engine/builtins/array_buffer/typed_array/mutation.rs b/src/engine/builtins/array_buffer/typed_array/mutation.rs index 9ab637bd..93d8032b 100644 --- a/src/engine/builtins/array_buffer/typed_array/mutation.rs +++ b/src/engine/builtins/array_buffer/typed_array/mutation.rs @@ -13,7 +13,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -53,7 +53,7 @@ impl Runtime { ) -> Result { let current = self.typed_array_state(&target)?; if current.out_of_bounds { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "out of bound", @@ -84,7 +84,9 @@ impl Runtime { let access = self.snapshot_buffer_access(current.snapshot.buffer)?; self.move_buffer_range(&access, &access, source_start, target_start, byte_count)?; } - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target.clone()))?, + )) } pub(crate) fn call_typed_array_fill( @@ -110,7 +112,7 @@ impl Runtime { ) -> Result { let current = self.typed_array_state(&target)?; if current.out_of_bounds { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "out of bound", @@ -134,7 +136,9 @@ impl Runtime { } })?; } - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target.clone()))?, + )) } pub(crate) fn call_typed_array_reverse( @@ -147,13 +151,16 @@ impl Runtime { "TypedArray.prototype.reverse received a constructor invocation", )); }; - let target = match self.require_typed_array_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let target = match self.require_typed_array_borrowed(realm, &this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let current = self.typed_array_state(target)?; if current.out_of_bounds { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached or resized", @@ -178,7 +185,9 @@ impl Runtime { } })?; } - Ok(Completion::Return(Value::Object(target.clone()))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target.clone()))?, + )) } } #[derive(Clone, Copy)] @@ -198,12 +207,12 @@ impl TypedMutationKind { pub(crate) enum TypedMutationStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: TypedMutationResume, }, Element { element: TypedArrayElementKind, - value: Value, + value: JsValue, resume: TypedMutationResume, }, } @@ -268,51 +277,47 @@ impl TypedMutationStep { "TypedArray mutation received a constructor invocation", )); }; - let target = match runtime.require_typed_array(realm, this_value.clone())? { + let target = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = match runtime.typed_array_validated_length(realm, &target)? { NativeConversion::Value(value) => i64::from(value), - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; - let first = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray mutation first argv was not padded", - ))? - .clone(); + let first = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray mutation first argv was not padded"), + )?)?; let end = if arguments.actual_arg_count > 2 - && !matches!(arguments.readable.get(2), Some(Value::Undefined)) + && !matches!(arguments.readable.get(2), Some(JsValue::Undefined)) { - Some( - arguments - .readable - .get(2) - .ok_or(RuntimeError::Invariant( - "TypedArray mutation end argv was missing", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(2).ok_or( + RuntimeError::Invariant("TypedArray mutation end argv was missing"), + )?)?) } else { None }; Ok(match kind { TypedMutationKind::CopyWithin => Self::Primitive { - value: first, + value: runtime.into_jsvalue(first)?, resume: TypedMutationResume(Box::new(TypedMutationResumeState { realm, target, length, phase: Phase::To { - from: arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( + from: runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant( "TypedArray.copyWithin start argv was not padded", - ))? - .clone(), + ), + )?)?, end, }, })), @@ -320,21 +325,15 @@ impl TypedMutationStep { TypedMutationKind::Fill => { let element = runtime.typed_array_snapshot(&target)?.element; let start = if arguments.actual_arg_count > 1 { - Some( - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray.fill start argv was missing", - ))? - .clone(), - ) + Some(runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("TypedArray.fill start argv was missing"), + )?)?) } else { None }; Self::Element { element, - value: first, + value: runtime.into_jsvalue(first)?, resume: TypedMutationResume(Box::new(TypedMutationResumeState { realm, target, @@ -359,7 +358,9 @@ impl TypedMutationResume { let bytes = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedMutationStep::Complete(Completion::Throw(value))); + return Ok(TypedMutationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::FillValue { @@ -382,9 +383,12 @@ impl TypedMutationResume { self }; if let Some(value) = start { - Ok(TypedMutationStep::Primitive { value, resume }) + Ok(TypedMutationStep::Primitive { + value: runtime.into_jsvalue(value)?, + resume, + }) } else { - resume.resume(runtime, Completion::Return(Value::Int(0))) + resume.resume(runtime, Completion::Return(JsValue::Int(0))) } } pub(crate) fn resume( @@ -393,7 +397,7 @@ impl TypedMutationResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedMutationStep::Complete(Completion::Throw(value))); } @@ -407,12 +411,14 @@ impl TypedMutationResume { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedMutationStep::Complete(Completion::Throw(value))); + return Ok(TypedMutationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { Phase::To { from, end } => Ok(TypedMutationStep::Primitive { - value: from, + value: runtime.into_jsvalue(from)?, resume: { let updated_0 = Phase::From { to: index, end }; self.0.phase = updated_0; @@ -422,7 +428,7 @@ impl TypedMutationResume { Phase::From { to, end } => { if let Some(value) = end { Ok(TypedMutationStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: { let updated_0 = Phase::CopyEnd { to, from: index }; self.0.phase = updated_0; @@ -459,7 +465,7 @@ impl TypedMutationResume { } => { if let Some(value) = end { Ok(TypedMutationStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: { let updated_0 = Phase::FillEnd { element, @@ -508,8 +514,8 @@ fn finish( step = match step { TypedMutationStep::Complete(result) => return Ok(result), TypedMutationStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; @@ -519,10 +525,13 @@ fn finish( element, value, resume, - } => resume.element( - runtime, - runtime.typed_array_convert_element(realm, element, &value)?, - )?, + } => { + let value = runtime.root_and_release_jsvalue(value)?; + resume.element( + runtime, + runtime.typed_array_convert_element(realm, element, &value)?, + )? + } }; } } diff --git a/src/engine/builtins/array_buffer/typed_array/search.rs b/src/engine/builtins/array_buffer/typed_array/search.rs index 2508aa8e..ada367a3 100644 --- a/src/engine/builtins/array_buffer/typed_array/search.rs +++ b/src/engine/builtins/array_buffer/typed_array/search.rs @@ -12,7 +12,7 @@ use crate::engine::{ builtins::native::ArraySearchKind, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -81,7 +81,7 @@ impl Runtime { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } } } else { @@ -100,14 +100,14 @@ impl Runtime { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } } } else { initial_length - 1 }; if index < 0 { - return Ok(Completion::Return(not_found())); + return Ok(Completion::Return(self.into_jsvalue(not_found())?)); } (index, -1) } @@ -122,12 +122,12 @@ impl Runtime { && initial_length > current_length && index < initial_length { - return Ok(Completion::Return(Value::Bool(true))); + return Ok(Completion::Return(JsValue::Bool(true))); } let length = initial_length.min(current_length); if length == 0 { - return Ok(Completion::Return(not_found())); + return Ok(Completion::Return(self.into_jsvalue(not_found())?)); } let end = match kind { ArraySearchKind::Includes | ArraySearchKind::IndexOf => { @@ -157,16 +157,17 @@ impl Runtime { } }; if matches { - return Ok(Completion::Return(match kind { + let result = match kind { ArraySearchKind::Includes => Value::Bool(true), ArraySearchKind::IndexOf | ArraySearchKind::LastIndexOf => { Value::number(index as f64) } - })); + }; + return Ok(Completion::Return(self.into_jsvalue(result)?)); } index += step; } - Ok(Completion::Return(not_found())) + Ok(Completion::Return(self.into_jsvalue(not_found())?)) } } #[derive(Clone, Copy)] @@ -186,7 +187,7 @@ impl TypedSearchKind { pub(crate) enum TypedSearchStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: TypedSearchResume, }, } @@ -223,15 +224,19 @@ impl TypedSearchStep { "TypedArray search received a constructor invocation", )); }; - let target = match runtime.require_typed_array(realm, this_value.clone())? { + let target = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = if matches!(kind, TypedSearchKind::At) { let initial = runtime.typed_array_state(&target)?; if initial.out_of_bounds { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", @@ -243,19 +248,17 @@ impl TypedSearchStep { match runtime.typed_array_validated_length(realm, &target)? { NativeConversion::Value(value) => i64::from(value), NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } }; match kind { TypedSearchKind::At => Ok(Self::Primitive { - value: arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray.at index argv was not padded", - ))? - .clone(), + value: runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray.at index argv was not padded"), + )?)?, resume: TypedSearchResume(Box::new(TypedSearchResumeState { realm, target, @@ -266,18 +269,17 @@ impl TypedSearchStep { }), TypedSearchKind::Search(search_kind) => { if length == 0 { - return Ok(Self::Complete(Completion::Return(match search_kind { + let result = match search_kind { ArraySearchKind::Includes => Value::Bool(false), _ => Value::Int(-1), - }))); + }; + return Ok(Self::Complete(Completion::Return( + runtime.into_jsvalue(result)?, + ))); } - let search = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray search value argv was not padded", - ))? - .clone(); + let search = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray search value argv was not padded"), + )?)?; if arguments.actual_arg_count <= 1 { return Ok(Self::Complete(runtime.finish_typed_array_search( realm, @@ -289,13 +291,9 @@ impl TypedSearchStep { )?)); } Ok(Self::Primitive { - value: arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray search fromIndex argv was missing", - ))? - .clone(), + value: runtime.dup_jsvalue(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("TypedArray search fromIndex argv was missing"), + )?)?, resume: TypedSearchResume(Box::new(TypedSearchResumeState { realm, target, @@ -322,10 +320,13 @@ impl TypedSearchResume { }; Ok(TypedSearchStep::Complete(match self.0.kind { TypedSearchKind::At => { + let value = runtime.root_and_release_jsvalue(value)?; let index = match runtime.native_to_int64_sat(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSearchStep::Complete(Completion::Throw(value))); + return Ok(TypedSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let index = if index < 0 { @@ -334,18 +335,17 @@ impl TypedSearchResume { index }; if index < 0 { - Completion::Return(Value::Undefined) + Completion::Return(JsValue::Undefined) } else { - Completion::Return( - runtime - .typed_array_read_index( - &self.0.target, - u64::try_from(index).map_err(|_| { - RuntimeError::Invariant("TypedArray.at index overflowed u64") - })?, - )? - .unwrap_or(Value::Undefined), - ) + let value = runtime + .typed_array_read_index( + &self.0.target, + u64::try_from(index).map_err(|_| { + RuntimeError::Invariant("TypedArray.at index overflowed u64") + })?, + )? + .unwrap_or(Value::Undefined); + Completion::Return(runtime.into_jsvalue(value)?) } } TypedSearchKind::Search(kind) => runtime.finish_typed_array_search( @@ -354,7 +354,7 @@ impl TypedSearchResume { self.0.target, self.0.length, self.0.search, - Some(value), + Some(runtime.root_value(&value)?), )?, })) } @@ -368,8 +368,8 @@ fn finish( step = match step { TypedSearchStep::Complete(result) => return Ok(result), TypedSearchStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/array_buffer/typed_array/set.rs b/src/engine/builtins/array_buffer/typed_array/set.rs index 4640b3ba..f2fa80eb 100644 --- a/src/engine/builtins/array_buffer/typed_array/set.rs +++ b/src/engine/builtins/array_buffer/typed_array/set.rs @@ -4,7 +4,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -13,7 +13,7 @@ use crate::engine::{ pub(crate) enum TypedSetStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: TypedSetResume, }, Read { @@ -23,7 +23,7 @@ pub(crate) enum TypedSetStep { }, Element { element: TypedArrayElementKind, - value: Value, + value: JsValue, resume: TypedSetResume, }, } @@ -71,24 +71,20 @@ impl TypedSetStep { "TypedArray.prototype.set received a constructor invocation", )); }; - let target = match runtime.require_typed_array(realm, this_value.clone())? { + let target = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; - let value = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray.set offset argv was not padded", - ))? - .clone(); - let source = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray.set source argv was not padded", - ))? - .clone(); + let value = runtime.dup_jsvalue(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("TypedArray.set offset argv was not padded"), + )?)?; + let source = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray.set source argv was not padded"), + )?)?; Ok(Self::Primitive { value, resume: TypedSetResume(Box::new(TypedSetResumeState { @@ -105,7 +101,9 @@ impl TypedSetResume { state: SetState, ) -> Result { if state.index == state.length { - return Ok(TypedSetStep::Complete(Completion::Return(Value::Undefined))); + return Ok(TypedSetStep::Complete(Completion::Return( + JsValue::Undefined, + ))); } Ok(TypedSetStep::Read { object: state.source.clone(), @@ -122,7 +120,7 @@ impl TypedSetResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedSetStep::Complete(Completion::Throw(value))); } @@ -138,12 +136,14 @@ impl TypedSetResume { let offset = match runtime.native_to_int64_sat(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSetStep::Complete(Completion::Throw(value))); + return Ok(TypedSetStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if offset < 0 { return Ok(TypedSetStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid offset", @@ -154,7 +154,9 @@ impl TypedSetResume { let target_length = match runtime.typed_array_validated_length(realm, &target)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSetStep::Complete(Completion::Throw(value))); + return Ok(TypedSetStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if let Value::Object(source_object) = &source @@ -177,7 +179,9 @@ impl TypedSetResume { let source = match runtime.native_to_object(realm, source)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSetStep::Complete(Completion::Throw(value))); + return Ok(TypedSetStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let key = @@ -199,7 +203,7 @@ impl TypedSetResume { }) } SetPhase::Length(state) => Ok(TypedSetStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: Self(Box::new(TypedSetResumeState { realm, phase: SetPhase::LengthNumber(state), @@ -214,7 +218,9 @@ impl TypedSetResume { state.length = match runtime.native_to_length(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSetStep::Complete(Completion::Throw(value))); + return Ok(TypedSetStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if state @@ -223,14 +229,18 @@ impl TypedSetResume { .is_none_or(|end| end > u64::from(state.target_length)) { return Ok(TypedSetStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Range, "out of bound")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Range, + "out of bound", + )?, ))); } Self::next(runtime, realm, state) } SetPhase::Read(state) => Ok(TypedSetStep::Element { element: runtime.typed_array_snapshot(&state.target)?.element, - value, + value: runtime.into_jsvalue(value)?, resume: Self(Box::new(TypedSetResumeState { realm, phase: SetPhase::Write(state), @@ -249,7 +259,9 @@ impl TypedSetResume { let bytes = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSetStep::Complete(Completion::Throw(value))); + return Ok(TypedSetStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let SetPhase::Write(mut state) = self.0.phase else { @@ -275,8 +287,8 @@ pub(super) fn finish( step = match step { TypedSetStep::Complete(result) => return Ok(result), TypedSetStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/array_buffer/typed_array/slice.rs b/src/engine/builtins/array_buffer/typed_array/slice.rs index eda5b744..589fbee5 100644 --- a/src/engine/builtins/array_buffer/typed_array/slice.rs +++ b/src/engine/builtins/array_buffer/typed_array/slice.rs @@ -13,7 +13,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -52,12 +52,14 @@ impl Runtime { let current_source_length = match self.typed_array_validated_length(realm, &source)? { NativeConversion::Value(value) => u64::from(value), NativeConversion::Throw(value) => { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } }; match self.typed_array_validated_length(realm, &target)? { NativeConversion::Value(_) => {} - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } let start = u64::try_from(start) @@ -80,14 +82,16 @@ impl Runtime { match self.typed_array_set_index(realm, &target, index, &value)? { NativeConversion::Value(()) => {} NativeConversion::Throw(value) => { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } } } } } } - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(target))?, + )) } pub(crate) fn call_typed_array_subarray( @@ -208,36 +212,34 @@ impl TypedSliceStep { "TypedArray slice received a constructor invocation", )); }; - let source = match runtime.require_typed_array(realm, this_value.clone())? { + let source = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = match kind { TypedSliceKind::Slice => match runtime.typed_array_validated_length(realm, &source)? { NativeConversion::Value(value) => i64::from(value), NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }, TypedSliceKind::Subarray => i64::from(runtime.typed_array_state(&source)?.length), }; // A branded view's raw metadata is immutable; its backing state is reread after every conversion. runtime.typed_array_snapshot(&source)?; - let end = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray slice end argv was not padded", - ))? - .clone(); + let end = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("TypedArray slice end argv was not padded"), + )?)?; Ok(Self::request_primitive( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray slice start argv was not padded", - ))? - .clone(), + runtime.dup_jsvalue(arguments.readable.first().ok_or(RuntimeError::Invariant( + "TypedArray slice start argv was not padded", + ))?)?, TypedSliceResume(Box::new(TypedSliceResumeState { pending_effect: TypedSliceStepPending::default(), realm, @@ -256,7 +258,7 @@ impl TypedSliceResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedSliceStep::Complete(Completion::Throw(value))); } @@ -270,7 +272,9 @@ impl TypedSliceResume { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSliceStep::Complete(Completion::Throw(value))); + return Ok(TypedSliceStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -308,7 +312,10 @@ impl TypedSliceResume { let length = next.length; return next.select(runtime, index, offset, length, true); } - Ok(TypedSliceStep::request_primitive(end, next)) + Ok(TypedSliceStep::request_primitive( + runtime.into_jsvalue(end)?, + next, + )) } Phase::End { start, offset } => self.select(runtime, start, offset, index, false), Phase::Species { .. } => Err(RuntimeError::Invariant( @@ -360,7 +367,9 @@ impl TypedSliceResume { let target = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSliceStep::Complete(Completion::Throw(value))); + return Ok(TypedSliceStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Species { start, count } = self.0.phase else { @@ -372,7 +381,9 @@ impl TypedSliceResume { TypedSliceKind::Slice => { runtime.finish_typed_slice(self.0.realm, self.0.source, target, start, count)? } - TypedSliceKind::Subarray => Completion::Return(Value::Object(target)), + TypedSliceKind::Subarray => { + Completion::Return(runtime.into_jsvalue(Value::Object(target))?) + } })) } } @@ -387,8 +398,8 @@ fn finish( TypedSliceStep::Primitive { mut resume } => { let value = resume.take_primitive_value(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)? } else { Completion::Return(value) }; @@ -423,7 +434,7 @@ fn finish( #[derive(Default)] struct TypedSliceStepPending { - primitive_value: Option, + primitive_value: Option, species_source: Option, species_element: Option, species_length: Option, @@ -434,7 +445,7 @@ struct TypedSliceStepPending { species_view_length: Option>, } impl TypedSliceStep { - pub(crate) fn request_primitive(value: Value, mut resume: TypedSliceResume) -> Self { + pub(crate) fn request_primitive(value: JsValue, mut resume: TypedSliceResume) -> Self { resume.0.pending_effect.primitive_value = Some(value); Self::Primitive { resume } } @@ -466,7 +477,7 @@ impl TypedSliceStep { } } impl TypedSliceResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .pending_effect .primitive_value diff --git a/src/engine/builtins/array_buffer/typed_array/sort.rs b/src/engine/builtins/array_buffer/typed_array/sort.rs index 3834c972..fc553bae 100644 --- a/src/engine/builtins/array_buffer/typed_array/sort.rs +++ b/src/engine/builtins/array_buffer/typed_array/sort.rs @@ -22,7 +22,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{CallableRef, ObjectRef}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -392,11 +392,11 @@ pub(crate) enum TypedSortStep { Complete(Completion), Call { callable: CallableRef, - arguments: Vec, + arguments: Vec, resume: TypedSortResume, }, Number { - value: Value, + value: JsValue, resume: TypedSortResume, }, } @@ -442,7 +442,9 @@ impl TypedSortStep { match runtime.native_sort_comparator(realm, arguments)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } else { @@ -453,9 +455,13 @@ impl TypedSortStep { "TypedArray sort requires generic invocation", )); }; - let source = match runtime.require_typed_array(realm, this_value.clone())? { + let source = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let (target, length, comparator) = if copying { let source_state = runtime.typed_array_state(&source)?; @@ -467,13 +473,17 @@ impl TypedSortStep { )? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let comparator = match runtime.native_sort_comparator(realm, arguments)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let length = runtime.typed_array_state(&target)?.length; @@ -482,17 +492,23 @@ impl TypedSortStep { let length = match runtime.typed_array_validated_length(realm, &source)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; (source, length, comparator) }; if length < 2 { - return Ok(Self::Complete(Completion::Return(Value::Object(target)))); + return Ok(Self::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(target))?, + ))); } let Some(comparator) = comparator else { runtime.sort_typed_array_words_default(&target, length)?; - return Ok(Self::Complete(Completion::Return(Value::Object(target)))); + return Ok(Self::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(target))?, + ))); }; let initial = runtime.typed_array_state(&target)?; if initial.out_of_bounds || initial.length < length { @@ -533,9 +549,9 @@ impl TypedSortResume { &self.0.indices, self.0.width, )?; - return Ok(TypedSortStep::Complete(Completion::Return(Value::Object( - self.0.target, - )))); + return Ok(TypedSortStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.0.target))?, + ))); } SortAction::Swap(left, right) => self.0.indices.swap(left, right), SortAction::Compare(left, right) => { @@ -564,7 +580,10 @@ impl TypedSortResume { self.0.phase = TypedSortPhase::Call; return Ok(TypedSortStep::Call { callable: self.0.comparator.clone(), - arguments: vec![left_value, right_value], + arguments: vec![ + runtime.into_jsvalue(left_value)?, + runtime.into_jsvalue(right_value)?, + ], resume: self, }); } @@ -587,7 +606,7 @@ impl TypedSortResume { return Ok(TypedSortStep::Complete(Completion::Throw(value))); } }; - if let Value::Int(value) = value { + if let JsValue::Int(value) = value { return self.compared(runtime, f64::from(value)); } self.0.phase = TypedSortPhase::Number; @@ -609,7 +628,9 @@ impl TypedSortResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedSortStep::Complete(Completion::Throw(value))); + return Ok(TypedSortStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.compared(runtime, value) @@ -637,11 +658,18 @@ pub(crate) fn finish( callable, arguments, resume, - } => resume.resume( - runtime, - runtime.call_internal(realm, &callable, Value::Undefined, &arguments)?, - )?, + } => { + let arguments = arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; + resume.resume( + runtime, + runtime.call_internal(realm, &callable, Value::Undefined, &arguments)?, + )? + } TypedSortStep::Number { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } }; diff --git a/src/engine/builtins/array_buffer/typed_array/species.rs b/src/engine/builtins/array_buffer/typed_array/species.rs index a94f589e..ed2171ee 100644 --- a/src/engine/builtins/array_buffer/typed_array/species.rs +++ b/src/engine/builtins/array_buffer/typed_array/species.rs @@ -9,7 +9,7 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::ConstructorRef}, }; @@ -106,7 +106,9 @@ impl Runtime { "out of memory", )?)); } - owned.extend_from_slice(arguments); + for argument in arguments { + owned.push(self.into_jsvalue(argument.clone())?); + } finish_species( self, realm, @@ -120,15 +122,21 @@ impl Runtime { minimum_length: Option, ) -> Result, RuntimeError> { let target = match result { - Completion::Return(Value::Object(value)) => value, - Completion::Return(_) => { - return Ok(NativeConversion::Throw(self.new_native_error( - realm, - NativeErrorKind::Type, - "not a TypedArray", - )?)); + Completion::Return(value) => match self.root_and_release_jsvalue(value)? { + Value::Object(object) => object, + _ => { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Type, + "not a TypedArray", + )?)); + } + }, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); } - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), }; let Some(_) = self.typed_array_snapshot_if_branded(&target)? else { return Ok(NativeConversion::Throw(self.new_native_error( @@ -248,7 +256,7 @@ pub(crate) enum TypedSpeciesStep { }, Construct { constructor: ConstructorRef, - arguments: Vec, + arguments: Vec, resume: TypedSpeciesResume, }, } @@ -347,7 +355,7 @@ impl TypedSpeciesStep { runtime: &Runtime, realm: ContextId, constructor: Value, - arguments: Vec, + arguments: Vec, minimum_length: Option, ) -> Result { let Value::Object(object) = constructor else { @@ -418,7 +426,7 @@ impl TypedSpeciesResume { } let minimum = match input.mode { SpeciesMode::Length(length) => { - arguments.push(Value::number(length as f64)); + arguments.push(runtime.into_jsvalue(Value::number(length as f64))?); Some(length) } SpeciesMode::View { @@ -426,10 +434,10 @@ impl TypedSpeciesResume { byte_offset, length, } => { - arguments.push(Value::Object(buffer)); - arguments.push(Value::number(byte_offset as f64)); + arguments.push(JsValue::Object(buffer.into_handle())); + arguments.push(runtime.into_jsvalue(Value::number(byte_offset as f64))?); if let Some(length) = length { - arguments.push(Value::number(length as f64)); + arguments.push(runtime.into_jsvalue(Value::number(length as f64))?); } None } @@ -444,15 +452,17 @@ impl TypedSpeciesResume { let value = match result { Completion::Return(value) => value, Completion::Throw(value) => { - return Ok(TypedSpeciesStep::Complete(NativeConversion::Throw(value))); + return Ok(TypedSpeciesStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } }; match self.0.phase { SpeciesPhase::Constructor(input) => { - if matches!(value, Value::Undefined) { + if matches!(value, JsValue::Undefined) { return Self::selected(runtime, self.0.realm, input, Value::Undefined); } - let Value::Object(object) = value else { + let Value::Object(object) = runtime.root_and_release_jsvalue(value)? else { return Ok(TypedSpeciesStep::Complete(NativeConversion::Throw( runtime.new_native_error( self.0.realm, @@ -470,7 +480,12 @@ impl TypedSpeciesResume { })), }) } - SpeciesPhase::Species(input) => Self::selected(runtime, self.0.realm, input, value), + SpeciesPhase::Species(input) => Self::selected( + runtime, + self.0.realm, + input, + runtime.root_and_release_jsvalue(value)?, + ), SpeciesPhase::Constructed(minimum) => Ok(TypedSpeciesStep::Complete( runtime.validate_typed_array_construction( self.0.realm, @@ -501,15 +516,21 @@ fn finish_species( constructor, arguments, resume, - } => resume.resume( - runtime, - runtime.construct_constructor_internal( - realm, - &constructor, - &constructor, - &arguments, - )?, - )?, + } => { + let arguments = arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; + resume.resume( + runtime, + runtime.construct_constructor_internal( + realm, + &constructor, + &constructor, + &arguments, + )?, + )? + } }; } } diff --git a/src/engine/builtins/array_buffer/typed_array/stringification.rs b/src/engine/builtins/array_buffer/typed_array/stringification.rs index f700ce49..7879f7e1 100644 --- a/src/engine/builtins/array_buffer/typed_array/stringification.rs +++ b/src/engine/builtins/array_buffer/typed_array/stringification.rs @@ -10,7 +10,7 @@ use crate::engine::{ builtins::native::ArrayJoinKind, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{JsString, JsStringBuilder, Value, conversion::NativeConversion}, + value::{JsString, JsStringBuilder, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{DirectCallTarget, NativeArguments, NativeInvocation}, @@ -45,18 +45,20 @@ impl Runtime { arguments: &NativeArguments, string_limit: usize, ) -> Result { - finish( - self, - realm, - TypedStringStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + finish( self, realm, - kind, - &invocation, - arguments, - string_limit, - )?, - ) + TypedStringStep::start_with_limit( + self, + realm, + kind, + invocation, + arguments, + string_limit, + )?, + ) + }) } } pub(crate) enum TypedStringStep { @@ -92,7 +94,7 @@ pub(crate) struct TypedStringResumeState { } enum Phase { Separator, - LocaleMethod(Value), + LocaleMethod(JsValue), LocaleResult, Element, } @@ -126,16 +128,24 @@ impl TypedStringStep { "TypedArray stringification received a constructor invocation", )); }; - let target = match runtime.require_typed_array(realm, this_value.clone())? { + let target = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = match runtime.typed_array_validated_length(realm, &target)? { NativeConversion::Value(value) => u64::from(value), - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let state = TypedStringResume(Box::new(TypedStringResumeState { - pending_effect: TypedStringStepPending::default(), + pending_effect: TypedStringStepPending::new(runtime.clone()), realm, target, kind, @@ -148,16 +158,12 @@ impl TypedStringStep { })); if matches!(kind, ArrayJoinKind::Join) && arguments.actual_arg_count != 0 - && !matches!(arguments.readable.first(), Some(Value::Undefined)) + && !matches!(arguments.readable.first(), Some(JsValue::Undefined)) { return Ok(Self::request_primitive( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray.join separator argv was not padded", - ))? - .clone(), + runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("TypedArray.join separator argv was not padded"), + )?)?, state, )); } @@ -181,7 +187,9 @@ impl TypedStringResume { let string = match runtime.native_to_js_string(self.0.realm, &element)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedStringStep::Complete(Completion::Throw(value))); + return Ok(TypedStringStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.output.push_js_string(&string)?; @@ -191,8 +199,9 @@ impl TypedStringResume { let key = runtime.pinned_property_key( crate::engine::atom::pinned::PinnedAtom::ToLocaleString, )?; - self.0.phase = Phase::LocaleMethod(element.clone()); - return Ok(TypedStringStep::request_read(element, key, self)); + let receiver = runtime.into_jsvalue(element)?; + self.0.phase = Phase::LocaleMethod(runtime.dup_jsvalue(&receiver)?); + return Ok(TypedStringStep::request_read(receiver, key, self)); } } } @@ -200,7 +209,7 @@ impl TypedStringResume { self.0.output.push_js_string(&self.0.separator)?; } Ok(TypedStringStep::Complete(Completion::Return( - Value::String(self.0.output.finish()?), + runtime.into_jsvalue(Value::String(self.0.output.finish()?))?, ))) } pub(crate) fn resume( @@ -209,7 +218,7 @@ impl TypedStringResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedStringStep::Complete(Completion::Throw(value))); } @@ -219,7 +228,9 @@ impl TypedStringResume { self.0.separator = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedStringStep::Complete(Completion::Throw(value))); + return Ok(TypedStringStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.current_length = @@ -233,7 +244,7 @@ impl TypedStringResume { }; let Some(callable) = callable else { return Ok(TypedStringStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not a function", @@ -250,13 +261,18 @@ impl TypedStringResume { } Phase::LocaleResult => { self.0.phase = Phase::Element; - Ok(TypedStringStep::request_primitive(value, self)) + Ok(TypedStringStep::request_primitive( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Element => { let string = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedStringStep::Complete(Completion::Throw(value))); + return Ok(TypedStringStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.output.push_js_string(&string)?; @@ -277,8 +293,8 @@ fn finish( TypedStringStep::Primitive { mut resume } => { let value = resume.take_primitive_value(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::String)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? } else { Completion::Return(value) }; @@ -286,7 +302,7 @@ fn finish( } } TypedStringStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -295,8 +311,12 @@ fn finish( } TypedStringStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -313,22 +333,57 @@ fn finish( } } -#[derive(Default)] struct TypedStringStepPending { - primitive_value: Option, - read_receiver: Option, + runtime: Runtime, + primitive_value: Option, + read_receiver: Option, read_key: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, +} +impl TypedStringStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + primitive_value: None, + read_receiver: None, + read_key: None, + call_target: None, + call_receiver: None, + call_arguments: None, + } + } +} +impl Drop for TypedStringStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow. + fn drop(&mut self) { + for value in [ + self.primitive_value.take(), + self.read_receiver.take(), + self.call_receiver.take(), + ] + .into_iter() + .flatten() + { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } } impl TypedStringStep { - pub(crate) fn request_primitive(value: Value, mut resume: TypedStringResume) -> Self { + pub(crate) fn request_primitive(value: JsValue, mut resume: TypedStringResume) -> Self { resume.0.pending_effect.primitive_value = Some(value); Self::Primitive { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: TypedStringResume, ) -> Self { @@ -338,8 +393,8 @@ impl TypedStringStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: TypedStringResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -349,14 +404,14 @@ impl TypedStringStep { } } impl TypedStringResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .pending_effect .primitive_value .take() .expect("TypedStringStep Primitive value") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -377,14 +432,14 @@ impl TypedStringResume { .take() .expect("TypedStringStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("TypedStringStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/array_buffer/typed_array/stringification/tests.rs b/src/engine/builtins/array_buffer/typed_array/stringification/tests.rs index 6edb3911..e68d7178 100644 --- a/src/engine/builtins/array_buffer/typed_array/stringification/tests.rs +++ b/src/engine/builtins/array_buffer/typed_array/stringification/tests.rs @@ -543,7 +543,7 @@ fn typed_array_separator_overflow_stops_before_the_next_locale_call() { context.realm, ArrayJoinKind::ToLocaleString, NativeInvocation::Call { - this_value: Value::Object(source), + this_value: runtime.into_jsvalue(Value::Object(source)).unwrap(), }, &NativeArguments { actual_arg_count: 0, diff --git a/src/engine/builtins/array_buffer/typed_array/traversal.rs b/src/engine/builtins/array_buffer/typed_array/traversal.rs index 79a119f8..82b45606 100644 --- a/src/engine/builtins/array_buffer/typed_array/traversal.rs +++ b/src/engine/builtins/array_buffer/typed_array/traversal.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::{ArrayFindKind, ArrayReduceKind}, heap::ContextId, object::{CallableRef, ObjectRef}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{DirectCallTarget, NativeArguments, NativeInvocation}, @@ -74,31 +74,31 @@ impl TypedTraversalStep { "TypedArray traversal received a constructor invocation", )); }; - let target = match runtime.require_typed_array(realm, this_value.clone())? { + let target = match runtime.require_typed_array(realm, runtime.root_value(this_value)?)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let length = match runtime.typed_array_validated_length(realm, &target)? { NativeConversion::Value(value) => u64::from(value), - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; - let callback = runtime.callable_from_value( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "TypedArray traversal callback argv was not padded", - ))? - .clone(), - )?; + let callback = runtime.callable_from_value(runtime.root_value( + arguments.readable.first().ok_or(RuntimeError::Invariant( + "TypedArray traversal callback argv was not padded", + ))?, + )?)?; let second = if arguments.actual_arg_count > 1 { - arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "TypedArray traversal second argument was missing", - ))? - .clone() + runtime.root_value(arguments.readable.get(1).ok_or(RuntimeError::Invariant( + "TypedArray traversal second argument was missing", + ))?)? } else { Value::Undefined }; @@ -124,7 +124,7 @@ impl TypedTraversalStep { } else { if length == 0 { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "empty array", @@ -169,14 +169,15 @@ impl TraversalState { } fn find(mut self, runtime: &Runtime) -> Result { if self.step == self.length { + let result = match self.kind { + TypedTraversalKind::Find(ArrayFindKind::Find | ArrayFindKind::FindLast) => { + Value::Undefined + } + TypedTraversalKind::Find(_) => Value::Int(-1), + _ => return Err(RuntimeError::Invariant("TypedArray find lost its selector")), + }; return Ok(TypedTraversalStep::Complete(Completion::Return( - match self.kind { - TypedTraversalKind::Find(ArrayFindKind::Find | ArrayFindKind::FindLast) => { - Value::Undefined - } - TypedTraversalKind::Find(_) => Value::Int(-1), - _ => return Err(RuntimeError::Invariant("TypedArray find lost its selector")), - }, + runtime.into_jsvalue(result)?, ))); } let index = self.index(); @@ -187,7 +188,9 @@ impl TraversalState { let mut arguments = match self.arguments(runtime, 3)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedTraversalStep::Complete(Completion::Throw(value))); + return Ok(TypedTraversalStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; arguments.push(value.clone()); @@ -195,8 +198,11 @@ impl TraversalState { arguments.push(Value::Object(self.target.clone())); Ok(TypedTraversalStep::request_call( DirectCallTarget::Callable(self.callback.clone()), - self.this_arg.clone(), - arguments, + runtime.into_jsvalue(self.this_arg.clone())?, + arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, TypedTraversalResume(Box::new(TypedTraversalResumeState { pending_effect: TypedTraversalStepPending::default(), state: self, @@ -211,7 +217,7 @@ impl TraversalState { ) -> Result { if self.step == self.length { return Ok(TypedTraversalStep::Complete(Completion::Return( - accumulator, + runtime.into_jsvalue(accumulator)?, ))); } let index = self.index(); @@ -222,7 +228,9 @@ impl TraversalState { let mut arguments = match self.arguments(runtime, 4)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(TypedTraversalStep::Complete(Completion::Throw(value))); + return Ok(TypedTraversalStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; arguments.push(accumulator); @@ -231,8 +239,11 @@ impl TraversalState { arguments.push(Value::Object(self.target.clone())); Ok(TypedTraversalStep::request_call( DirectCallTarget::Callable(self.callback.clone()), - Value::Undefined, - arguments, + JsValue::Undefined, + arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, TypedTraversalResume(Box::new(TypedTraversalResumeState { pending_effect: TypedTraversalStepPending::default(), state: self, @@ -248,7 +259,7 @@ impl TypedTraversalResume { result: Completion, ) -> Result { let result = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(TypedTraversalStep::Complete(Completion::Throw(value))); } @@ -256,18 +267,19 @@ impl TypedTraversalResume { match self.0.phase { TraversalPhase::Find { value, index } => { if runtime.value_to_boolean(&result)? { + let found = match self.0.state.kind { + TypedTraversalKind::Find(ArrayFindKind::Find | ArrayFindKind::FindLast) => { + value + } + TypedTraversalKind::Find(_) => Value::number(index as f64), + _ => { + return Err(RuntimeError::Invariant( + "TypedArray find reply lost its selector", + )); + } + }; Ok(TypedTraversalStep::Complete(Completion::Return( - match self.0.state.kind { - TypedTraversalKind::Find( - ArrayFindKind::Find | ArrayFindKind::FindLast, - ) => value, - TypedTraversalKind::Find(_) => Value::number(index as f64), - _ => { - return Err(RuntimeError::Invariant( - "TypedArray find reply lost its selector", - )); - } - }, + runtime.into_jsvalue(found)?, ))) } else { self.0.state.find(runtime) @@ -287,8 +299,12 @@ pub(super) fn finish( TypedTraversalStep::Complete(result) => return Ok(result), TypedTraversalStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -308,14 +324,14 @@ pub(super) fn finish( #[derive(Default)] struct TypedTraversalStepPending { call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, } impl TypedTraversalStep { pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: TypedTraversalResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -332,14 +348,14 @@ impl TypedTraversalResume { .take() .expect("TypedTraversalStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("TypedTraversalStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/array_buffer/typed_array/uint8_codec.rs b/src/engine/builtins/array_buffer/typed_array/uint8_codec.rs index 8a7a56e2..03d994ca 100644 --- a/src/engine/builtins/array_buffer/typed_array/uint8_codec.rs +++ b/src/engine/builtins/array_buffer/typed_array/uint8_codec.rs @@ -11,7 +11,7 @@ use crate::engine::{ builtins::native::{TypedArrayElementKind, Uint8ArrayCodecKind}, heap::ContextId, object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -81,17 +81,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - codec_finish( - self, - realm, - Uint8CodecStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + codec_finish( self, realm, - Uint8ArrayCodecKind::FromBase64, - &invocation, - arguments, - )?, - ) + Uint8CodecStep::start( + self, + realm, + Uint8ArrayCodecKind::FromBase64, + invocation, + arguments, + )?, + ) + }) } fn call_uint8_array_from_hex( @@ -100,14 +102,18 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Uint8Array.fromHex received a constructor invocation", )); }; + invocation.release(self)?; let source = match self.uint8_codec_input_bytes(realm, arguments, 0)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let output_capacity = source .len() @@ -118,11 +124,13 @@ impl Runtime { ))?; let mut output = match self.uint8_codec_zeroed_bytes(realm, output_capacity)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let progress = decode_hex(&source, &mut output); if progress.invalid { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Syntax, "invalid hex string", @@ -131,9 +139,13 @@ impl Runtime { output.truncate(progress.written); let result = match self.new_uint8_array_from_codec_bytes(realm, &output)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - Ok(Completion::Return(Value::Object(result))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )) } fn call_uint8_array_set_from_base64( @@ -142,17 +154,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - codec_finish( - self, - realm, - Uint8CodecStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + codec_finish( self, realm, - Uint8ArrayCodecKind::SetFromBase64, - &invocation, - arguments, - )?, - ) + Uint8CodecStep::start( + self, + realm, + Uint8ArrayCodecKind::SetFromBase64, + invocation, + arguments, + )?, + ) + }) } fn call_uint8_array_set_from_hex( @@ -163,15 +177,21 @@ impl Runtime { ) -> Result { let target = match self.require_uint8_array_receiver(realm, invocation)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let source = match self.uint8_codec_input_bytes(realm, arguments, 0)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let state = match self.validated_uint8_codec_state(realm, &target)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let start = typed_array_absolute_byte_offset(state.snapshot, 0)?; let length = usize::try_from(state.byte_length) @@ -180,7 +200,7 @@ impl Runtime { let progress = self .with_buffer_range_mut(&access, start, length, |target| decode_hex(&source, target))?; if progress.invalid { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Syntax, "invalid hex string", @@ -195,17 +215,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - codec_finish( - self, - realm, - Uint8CodecStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + codec_finish( self, realm, - Uint8ArrayCodecKind::ToBase64, - &invocation, - arguments, - )?, - ) + Uint8CodecStep::start( + self, + realm, + Uint8ArrayCodecKind::ToBase64, + invocation, + arguments, + )?, + ) + }) } fn call_uint8_array_to_hex( @@ -215,11 +237,15 @@ impl Runtime { ) -> Result { let target = match self.require_uint8_array_receiver(realm, invocation)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let state = match self.validated_uint8_codec_state(realm, &target)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let length = usize::try_from(state.byte_length) .map_err(|_| RuntimeError::Invariant("Uint8Array byte length overflowed usize"))?; @@ -227,7 +253,7 @@ impl Runtime { "Uint8Array.toHex output length overflowed usize", ))?; if output_length > JsString::MAX_LEN { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "output too large", @@ -235,16 +261,18 @@ impl Runtime { } let mut output = match self.uint8_codec_zeroed_bytes(realm, output_length)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let start = typed_array_absolute_byte_offset(state.snapshot, 0)?; let access = self.snapshot_buffer_access(state.snapshot.buffer)?; self.with_buffer_range(&access, start, length, |source| { encode_hex(source, &mut output) })?; - Ok(Completion::Return(Value::String( + Ok(Completion::Return(self.into_jsvalue(Value::String( JsString::from_owned_latin1(output), - ))) + ))?)) } fn require_uint8_array_receiver( @@ -257,6 +285,7 @@ impl Runtime { "Uint8Array codec received a constructor invocation", )); }; + let this_value = self.root_and_release_jsvalue(this_value)?; let Value::Object(target) = this_value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, @@ -303,7 +332,10 @@ impl Runtime { arguments: &NativeArguments, index: usize, ) -> Result>, RuntimeError> { - let Some(Value::String(source)) = arguments.readable.get(index) else { + let value = self.root_value(arguments.readable.get(index).ok_or( + RuntimeError::Invariant("Uint8Array codec input argv was not padded"), + )?)?; + let Value::String(source) = value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, @@ -332,15 +364,17 @@ impl Runtime { .ok_or(RuntimeError::Invariant( "Uint8Array codec options argv was not padded", ))? { - Value::Undefined => Ok(NativeConversion::Value(None)), - Value::Object(value) => Ok(NativeConversion::Value(Some(value.clone()))), - Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_) - | Value::BigInt(_) - | Value::String(_) - | Value::Symbol(_) => Ok(NativeConversion::Throw(self.new_native_error( + JsValue::Undefined => Ok(NativeConversion::Value(None)), + JsValue::Object(id) => Ok(NativeConversion::Value(Some( + ObjectRef::from_borrowed_handle(self.clone(), *id)?, + ))), + JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) + | JsValue::BigInt(_) + | JsValue::String(_) + | JsValue::Symbol(_) => Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, "options must be an object", @@ -365,11 +399,13 @@ impl Runtime { ))?; let mut output = match self.uint8_codec_zeroed_bytes(realm, output_capacity)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let progress = decode_base64(&source, &mut output, alphabet, last_chunk); if progress.invalid { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Syntax, "invalid base64 string", @@ -378,9 +414,13 @@ impl Runtime { output.truncate(progress.written); let result = match self.new_uint8_array_from_codec_bytes(realm, &output)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - Ok(Completion::Return(Value::Object(result))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )) } fn finish_uint8_array_set_from_base64( &self, @@ -392,7 +432,9 @@ impl Runtime { ) -> Result { let state = match self.validated_uint8_codec_state(realm, &target)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let start = typed_array_absolute_byte_offset(state.snapshot, 0)?; let length = usize::try_from(state.byte_length) @@ -402,7 +444,7 @@ impl Runtime { decode_base64(&source, target, alphabet, last_chunk) })?; if progress.invalid { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Syntax, "invalid base64 string", @@ -419,7 +461,9 @@ impl Runtime { ) -> Result { let state = match self.validated_uint8_codec_state(realm, &target)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let length = usize::try_from(state.byte_length) .map_err(|_| RuntimeError::Invariant("Uint8Array byte length overflowed usize"))?; @@ -431,7 +475,7 @@ impl Runtime { "Uint8Array.toBase64 output length overflowed usize", ))?; if output_length > JsString::MAX_LEN { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "output too large", @@ -439,7 +483,9 @@ impl Runtime { } let mut output = match self.uint8_codec_zeroed_bytes(realm, output_length)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let start = typed_array_absolute_byte_offset(state.snapshot, 0)?; let access = self.snapshot_buffer_access(state.snapshot.buffer)?; @@ -452,9 +498,9 @@ impl Runtime { output.truncate(output.len() - 1); } } - Ok(Completion::Return(Value::String( + Ok(Completion::Return(self.into_jsvalue(Value::String( JsString::from_owned_latin1(output), - ))) + ))?)) } fn uint8_codec_alphabet_value( &self, @@ -612,7 +658,9 @@ impl Runtime { )); } } - Ok(Completion::Return(Value::Object(result))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )) } } @@ -932,17 +980,17 @@ impl Uint8CodecStep { let mode = match kind { Uint8ArrayCodecKind::FromHex => { return runtime - .call_uint8_array_from_hex(realm, invocation.clone(), arguments) + .call_uint8_array_from_hex(realm, invocation.dup(runtime)?, arguments) .map(Self::Complete); } Uint8ArrayCodecKind::SetFromHex => { return runtime - .call_uint8_array_set_from_hex(realm, invocation.clone(), arguments) + .call_uint8_array_set_from_hex(realm, invocation.dup(runtime)?, arguments) .map(Self::Complete); } Uint8ArrayCodecKind::ToHex => { return runtime - .call_uint8_array_to_hex(realm, invocation.clone()) + .call_uint8_array_to_hex(realm, invocation.dup(runtime)?) .map(Self::Complete); } Uint8ArrayCodecKind::FromBase64 => { @@ -955,10 +1003,12 @@ impl Uint8CodecStep { } Uint8ArrayCodecKind::SetFromBase64 | Uint8ArrayCodecKind::ToBase64 => { let object = - match runtime.require_uint8_array_receiver(realm, invocation.clone())? { + match runtime.require_uint8_array_receiver(realm, invocation.dup(runtime)?)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if kind == Uint8ArrayCodecKind::SetFromBase64 { @@ -974,7 +1024,9 @@ impl Uint8CodecStep { match runtime.uint8_codec_input_bytes(realm, arguments, 0)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } }; @@ -984,7 +1036,11 @@ impl Uint8CodecStep { usize::from(!matches!(mode, CodecMode::To(_))), )? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let resume = Uint8CodecResume(Box::new(Uint8CodecResumeState { realm, @@ -1016,7 +1072,7 @@ impl Uint8CodecResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(Uint8CodecStep::Complete(Completion::Throw(value))); } @@ -1026,7 +1082,9 @@ impl Uint8CodecResume { match runtime.uint8_codec_alphabet_value(self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Uint8CodecStep::Complete(Completion::Throw(value))); + return Ok(Uint8CodecStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }, ); @@ -1056,7 +1114,9 @@ impl Uint8CodecResume { let last = match runtime.uint8_codec_last_chunk_value(self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Uint8CodecStep::Complete(Completion::Throw(value))); + return Ok(Uint8CodecStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.complete(runtime, alphabet, last, false) diff --git a/src/engine/builtins/array_buffer/typed_array/write.rs b/src/engine/builtins/array_buffer/typed_array/write.rs index def50400..80a3c4d3 100644 --- a/src/engine/builtins/array_buffer/typed_array/write.rs +++ b/src/engine/builtins/array_buffer/typed_array/write.rs @@ -5,14 +5,14 @@ use crate::engine::{ builtins::native::TypedArrayElementKind, heap::ContextId, object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, }; pub(crate) enum TypedWriteStep { Complete(NativeConversion), Element { element: TypedArrayElementKind, - value: Value, + value: JsValue, resume: TypedWriteResume, }, } @@ -44,7 +44,7 @@ impl TypedWriteStep { let element = runtime.typed_array_snapshot(&object)?.element; Ok(Self::Element { element, - value: value.clone(), + value: runtime.into_jsvalue(value.clone())?, resume: TypedWriteResume(Box::new(TypedWriteResumeState { object, index, @@ -78,7 +78,12 @@ impl TypedWriteStep { )); } let element = runtime.typed_array_snapshot(object)?.element; - let result = super::element::encode_primitive(runtime, realm, element, value.clone())?; + let result = super::element::encode_primitive( + runtime, + realm, + element, + runtime.unroot_value(value)?, + )?; finish_element(runtime, object, index, result) } pub(crate) fn define( @@ -104,7 +109,7 @@ impl TypedWriteStep { }; Ok(Self::Element { element: state.snapshot.element, - value: value.clone(), + value: runtime.into_jsvalue(value.clone())?, resume: TypedWriteResume(Box::new(TypedWriteResumeState { object, index: Some(index), @@ -124,7 +129,7 @@ impl TypedWriteStep { element, value, resume, - } if !matches!(value, Value::Object(_)) => { + } if !matches!(value, JsValue::Object(_)) => { let ElementStep::Complete(result) = ElementStep::start(runtime, realm, element, value)? else { @@ -359,10 +364,11 @@ mod tests { assert!(runtime.0.state.borrow().active_frames.is_empty()); } - fn take_element(step: TypedWriteStep) -> TypedWriteResume { - let TypedWriteStep::Element { resume, .. } = step else { + fn take_element(runtime: &Runtime, step: TypedWriteStep) -> TypedWriteResume { + let TypedWriteStep::Element { value, resume, .. } = step else { panic!("expected element conversion") }; + runtime.release_jsvalue(value).unwrap(); resume } #[test] @@ -392,7 +398,7 @@ mod tests { } else { TypedWriteStep::set(&runtime, view, Some(0), Value::Object(value)).unwrap() }; - let resume = take_element(step); + let resume = take_element(&runtime, step); runtime.run_gc().unwrap(); for id in [view_id, buffer_id, value_id] { assert!(runtime.0.state.borrow().heap.object(id).is_ok()); diff --git a/src/engine/builtins/atomics.rs b/src/engine/builtins/atomics.rs index efedcda2..c5a5b536 100644 --- a/src/engine/builtins/atomics.rs +++ b/src/engine/builtins/atomics.rs @@ -18,6 +18,7 @@ use crate::engine::builtins::native::{ use std::time::Duration; use super::*; +use crate::engine::value::JsValue; mod operation; #[cfg(test)] @@ -155,7 +156,6 @@ impl Runtime { } Ok(atomics) } - pub(crate) fn call_atomics_native( &self, realm: ContextId, @@ -163,11 +163,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Atomics method did not receive a generic invocation", )); }; + invocation.release(self)?; match kind { AtomicsNativeKind::Operation(operation) => { self.call_atomics_operation(realm, operation, arguments) @@ -435,17 +437,17 @@ impl Runtime { "Atomics.pause hint was not readable", ))?; let valid = match value { - Value::Undefined | Value::Int(_) => true, - Value::Float(value) => value.is_finite() && value.fract() == 0.0, - Value::Null - | Value::Bool(_) - | Value::BigInt(_) - | Value::String(_) - | Value::Symbol(_) - | Value::Object(_) => false, + JsValue::Undefined | JsValue::Int(_) => true, + JsValue::Float(value) => value.is_finite() && value.fract() == 0.0, + JsValue::Null + | JsValue::Bool(_) + | JsValue::BigInt(_) + | JsValue::String(_) + | JsValue::Symbol(_) + | JsValue::Object(_) => false, }; if !valid { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an integral number", @@ -453,7 +455,7 @@ impl Runtime { } } std::hint::spin_loop(); - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } fn call_atomics_wait( @@ -491,7 +493,7 @@ impl Runtime { let width = usize::from(access.snapshot.element.byte_length()); let offset = atomic_absolute_byte_offset(access)?; with_atomics_seq_cst(|| self.write_buffer_word(&access.buffer, offset, &bytes[..width]))?; - Ok(Completion::Return(stored_value)) + Ok(Completion::Return(self.into_jsvalue(stored_value)?)) } fn atomics_wait_converted( &self, @@ -503,7 +505,7 @@ impl Runtime { // QuickJS deliberately checks the host policy after every observable // conversion, even when the current memory value would be unequal. if !self.can_block() { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot block in this thread", @@ -529,9 +531,9 @@ impl Runtime { waiter::WaitOutcome::Ok => "ok", waiter::WaitOutcome::TimedOut => "timed-out", }; - Ok(Completion::Return(Value::String(JsString::from_static( - result, - )))) + Ok(Completion::Return(self.into_jsvalue(Value::String( + JsString::from_static(result), + ))?)) } fn atomics_notify_converted( &self, @@ -539,7 +541,7 @@ impl Runtime { count: i32, ) -> Result { if count == 0 || !access.buffer.is_shared() { - return Ok(Completion::Return(Value::Int(0))); + return Ok(Completion::Return(JsValue::Int(0))); } let backing_id = access .buffer @@ -554,7 +556,7 @@ impl Runtime { ); let notified = i32::try_from(notified) .map_err(|_| RuntimeError::Invariant("Atomics.notify waiter count overflowed i32"))?; - Ok(Completion::Return(Value::Int(notified))) + Ok(Completion::Return(JsValue::Int(notified))) } } diff --git a/src/engine/builtins/atomics/operation.rs b/src/engine/builtins/atomics/operation.rs index e749c924..29497afb 100644 --- a/src/engine/builtins/atomics/operation.rs +++ b/src/engine/builtins/atomics/operation.rs @@ -7,13 +7,19 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, builtins::native::{AtomicsNativeKind, AtomicsOperationKind}, heap::ContextId, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, ToPrimitiveHint, call::NativeArguments}, }; pub(crate) enum AtomicsStep { Complete(Completion), - Primitive { value: Value, resume: AtomicsResume }, - Number { value: Value, resume: AtomicsResume }, + Primitive { + value: JsValue, + resume: AtomicsResume, + }, + Number { + value: JsValue, + resume: AtomicsResume, + }, } enum Phase { Index, @@ -60,7 +66,11 @@ impl AtomicsStep { let mut resume = AtomicsResume(Box::new(AtomicsResumeState { realm, kind, - arguments: arguments.readable.clone(), + arguments: arguments + .readable + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()?, prepared: None, access: None, operand: [0; 8], @@ -69,7 +79,9 @@ impl AtomicsStep { if kind == AtomicsNativeKind::IsLockFree { resume.phase = Phase::Size; return Ok(Self::Number { - value: resume.argument(0, "Atomics.isLockFree size was not readable")?, + value: runtime.into_jsvalue( + resume.argument(0, "Atomics.isLockFree size was not readable")?, + )?, resume, }); } @@ -77,10 +89,10 @@ impl AtomicsStep { let view = resume.argument(0, "Atomics TypedArray was not readable")?; resume.prepared = Some(match runtime.atomics_prepare_access(realm, &view, mode)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(resume.abrupt(value)), + NativeConversion::Throw(value) => return resume.abrupt(runtime, value), }); Ok(Self::Primitive { - value: resume.argument(1, "Atomics index was not readable")?, + value: runtime.into_jsvalue(resume.argument(1, "Atomics index was not readable")?)?, resume, }) } @@ -105,8 +117,10 @@ impl AtomicsResume { "Atomics conversion lost its access", )) } - fn abrupt(self, value: Value) -> AtomicsStep { - AtomicsStep::Complete(Completion::Throw(value)) + fn abrupt(self, runtime: &Runtime, value: Value) -> Result { + Ok(AtomicsStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))) } fn modify( self, @@ -117,10 +131,15 @@ impl AtomicsResume { let access = self.access()?; match runtime.atomics_revalidate_after_value(self.0.realm, access)? { NativeConversion::Value(()) => {} - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), } Ok(AtomicsStep::Complete(Completion::Return( - runtime.atomics_modify(access, operation, self.0.operand, replacement)?, + runtime.into_jsvalue(runtime.atomics_modify( + access, + operation, + self.0.operand, + replacement, + )?)?, ))) } pub(crate) fn resume( @@ -129,8 +148,10 @@ impl AtomicsResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(self.abrupt(value)), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return self.abrupt(runtime, runtime.root_and_release_jsvalue(value)?); + } }; if matches!(value, Value::Object(_)) { return Err(RuntimeError::Invariant( @@ -141,7 +162,7 @@ impl AtomicsResume { Phase::Index => { let index = match runtime.native_to_index(self.0.realm, &value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; let prepared = self.0.prepared.take().ok_or(RuntimeError::Invariant( "Atomics index lost validation snapshot", @@ -154,12 +175,12 @@ impl AtomicsResume { self.mode(), )? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }, ); if self.0.kind == AtomicsNativeKind::Operation(AtomicsOperationKind::Load) { return Ok(AtomicsStep::Complete(Completion::Return( - runtime.atomics_load(self.access()?)?, + runtime.into_jsvalue(runtime.atomics_load(self.access()?)?)?, ))); } if self.0.kind == AtomicsNativeKind::Notify { @@ -171,13 +192,14 @@ impl AtomicsResume { } self.0.phase = Phase::Count; return Ok(AtomicsStep::Number { - value, + value: runtime.into_jsvalue(value)?, resume: self, }); } self.0.phase = Phase::Operand; Ok(AtomicsStep::Primitive { - value: self.argument(2, "Atomics operand was not readable")?, + value: runtime + .into_jsvalue(self.argument(2, "Atomics operand was not readable")?)?, resume: self, }) } @@ -186,12 +208,12 @@ impl AtomicsResume { let stored = if access.snapshot.element.is_bigint() { match runtime.native_to_bigint(self.0.realm, &value)? { NativeConversion::Value(value) => Value::BigInt(value), - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), } } else { let number = match runtime.native_to_number(self.0.realm, &value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; let integer = if number.is_nan() { 0.0 @@ -215,7 +237,7 @@ impl AtomicsResume { }; match runtime.atomics_revalidate_after_value(self.0.realm, access)? { NativeConversion::Value(()) => {} - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), } Ok(AtomicsStep::Complete( runtime.atomics_store_converted(access, stored, bytes)?, @@ -228,13 +250,15 @@ impl AtomicsResume { &value, )? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; match self.0.kind { AtomicsNativeKind::Operation(AtomicsOperationKind::CompareExchange) => { self.0.phase = Phase::Replacement; Ok(AtomicsStep::Primitive { - value: self.argument(3, "Atomics replacement was not readable")?, + value: runtime.into_jsvalue( + self.argument(3, "Atomics replacement was not readable")?, + )?, resume: self, }) } @@ -244,7 +268,9 @@ impl AtomicsResume { AtomicsNativeKind::Wait => { self.0.phase = Phase::Timeout; Ok(AtomicsStep::Number { - value: self.argument(3, "Atomics.wait timeout was not readable")?, + value: runtime.into_jsvalue( + self.argument(3, "Atomics.wait timeout was not readable")?, + )?, resume: self, }) } @@ -260,7 +286,7 @@ impl AtomicsResume { &value, )? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; self.modify( runtime, @@ -278,10 +304,10 @@ impl AtomicsResume { ) -> Result { let number = match result { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; Ok(AtomicsStep::Complete(match self.0.phase { - Phase::Size => Completion::Return(Value::Bool(matches!( + Phase::Size => Completion::Return(JsValue::Bool(matches!( atomic_to_int32_sat(number), 1 | 2 | 4 | 8 ))), @@ -309,9 +335,10 @@ pub(crate) fn finish( AtomicsStep::Complete(result) => return Ok(result), AtomicsStep::Primitive { value, resume } => resume.resume( runtime, - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)?, + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)?, )?, AtomicsStep::Number { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } }; diff --git a/src/engine/builtins/atomics/tests.rs b/src/engine/builtins/atomics/tests.rs index 7a1a145c..13cb30fa 100644 --- a/src/engine/builtins/atomics/tests.rs +++ b/src/engine/builtins/atomics/tests.rs @@ -1,4 +1,5 @@ use crate::engine::api::Context; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeCProto; use super::*; @@ -114,7 +115,8 @@ fn global_atomics_is_lazy_realm_local_and_has_the_pinned_surface() { let state = runtime.0.state.borrow(); let object = state.heap.object(global.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot].flags, PropertyFlags::data(true, false, true), diff --git a/src/engine/builtins/continuation.rs b/src/engine/builtins/continuation.rs index 237d3094..15a7a957 100644 --- a/src/engine/builtins/continuation.rs +++ b/src/engine/builtins/continuation.rs @@ -200,7 +200,7 @@ pub(crate) enum NativeStep { GlobalEval(crate::engine::value::Value), JsonRaw { - value: crate::engine::value::Value, + value: crate::engine::value::JsValue, resume: super::JsonRawResume, }, ObjectConstructor(super::ObjectConstructorStep), @@ -832,7 +832,7 @@ impl NativeOperation { Self::AsyncResume(kind) => NativeStep::Async(runtime.start_async_function_resume( realm, kind, - invocation.clone(), + invocation.dup(runtime)?, arguments, )?), Self::Promise(target) => { @@ -844,7 +844,7 @@ impl NativeOperation { NativeStep::GeneratorResume(runtime.start_generator_prototype_resume( realm, kind, - invocation.clone(), + invocation.dup(runtime)?, arguments, )?) } @@ -955,9 +955,18 @@ impl NativeOperation { Self::StringProtocol(kind) => NativeStep::StringProtocol( super::StringProtocolStep::start(runtime, realm, kind, invocation, arguments)?, ), - Self::GlobalEval => NativeStep::GlobalEval(arguments.readable[0].clone()), + Self::GlobalEval => { + let argument = &arguments.readable[0]; + if matches!(argument, crate::engine::value::JsValue::String(_)) { + NativeStep::GlobalEval(runtime.root_value(argument)?) + } else { + NativeStep::Complete(crate::engine::vm::Completion::Return( + runtime.dup_jsvalue(argument)?, + )) + } + } Self::JsonRaw => NativeStep::JsonRaw { - value: arguments.readable[0].clone(), + value: runtime.dup_jsvalue(&arguments.readable[0])?, resume: super::JsonRawResume::new(realm), }, @@ -1026,15 +1035,16 @@ impl NativeOperation { runtime, realm, kind, - invocation.clone(), + invocation.dup(runtime)?, arguments, )?, ), #[cfg(feature = "test262-host")] Self::EvalScript => NativeStep::EvalScript( crate::engine::api::test262_host::operation::EvalScriptStep::start( + runtime, realm, - invocation.clone(), + invocation.dup(runtime)?, arguments, )?, ), @@ -1043,13 +1053,13 @@ impl NativeOperation { runtime, realm, target, - invocation.clone(), + invocation.dup(runtime)?, arguments, )?) } Self::HostOutput(target) => NativeStep::Complete(runtime.call_qjs_output( target, - invocation.clone(), + invocation.dup(runtime)?, arguments, )?), Self::Pure(target) => { @@ -1116,7 +1126,7 @@ impl NativeOperation { runtime, realm, kind, invocation, arguments, )?), Self::ArraySpeciesGetter => { - NativeStep::Complete(runtime.call_array_species_getter(invocation.clone())?) + NativeStep::Complete(runtime.call_array_species_getter(invocation.dup(runtime)?)?) } Self::ArraySort(kind) => NativeStep::ArraySort(super::ArraySortStep::start( runtime, realm, kind, invocation, arguments, @@ -1164,13 +1174,13 @@ impl NativeOperation { } Self::PureIterator(target) => NativeStep::Raw(match target { NativeFunctionId::StringIteratorNext => { - runtime.call_string_iterator_next_raw(realm, invocation.clone())? + runtime.call_string_iterator_next_raw(realm, invocation.dup(runtime)?)? } NativeFunctionId::MapIteratorNext => { - runtime.call_map_iterator_next_raw(realm, invocation.clone())? + runtime.call_map_iterator_next_raw(realm, invocation.dup(runtime)?)? } NativeFunctionId::SetIteratorNext => { - runtime.call_set_iterator_next_raw(realm, invocation.clone())? + runtime.call_set_iterator_next_raw(realm, invocation.dup(runtime)?)? } _ => unreachable!("closed pure iterator registration"), }), @@ -1200,22 +1210,24 @@ impl NativeOperation { ), Self::Proxy(target) => NativeStep::Complete(match target { NativeFunctionId::ProxyConstructor => { - runtime.call_proxy_constructor(realm, invocation.clone(), arguments)? + runtime.call_proxy_constructor(realm, invocation.dup(runtime)?, arguments)? } NativeFunctionId::ProxyRevocable => { - runtime.call_proxy_revocable(realm, invocation.clone(), arguments)? + runtime.call_proxy_revocable(realm, invocation.dup(runtime)?, arguments)? + } + NativeFunctionId::ProxyRevoke => { + runtime.call_proxy_revoke(invocation.dup(runtime)?)? } - NativeFunctionId::ProxyRevoke => runtime.call_proxy_revoke(invocation.clone())?, _ => unreachable!("closed Proxy native registration"), }), Self::Invoke(kind) => NativeStep::Invoke(super::function::invoke::InvokeStep::start( runtime, realm, kind, invocation, arguments, )?), Self::ObjectValueOf => NativeStep::Complete( - runtime.call_object_prototype_value_of(realm, invocation.clone())?, + runtime.call_object_prototype_value_of(realm, invocation.dup(runtime)?)?, ), Self::ObjectIs => { - NativeStep::Complete(runtime.call_object_is(invocation.clone(), arguments)?) + NativeStep::Complete(runtime.call_object_is(invocation.dup(runtime)?, arguments)?) } Self::String(kind) => { NativeStep::String(ObjectStringStep::start(runtime, realm, kind, invocation)?) diff --git a/src/engine/builtins/continuation/output.rs b/src/engine/builtins/continuation/output.rs index 23a339ea..b5dc4df0 100644 --- a/src/engine/builtins/continuation/output.rs +++ b/src/engine/builtins/continuation/output.rs @@ -73,24 +73,25 @@ impl InitialOutput for crate::engine::builtins::ArrayNextStep { mod tests { use super::*; use crate::engine::api::{Runtime, Value}; + use crate::engine::value::JsValue; use crate::engine::vm::{Completion, call::NativeInvocation}; #[test] fn domain_completion_does_not_construct_a_waiting_payload() { let result = deliver( - crate::engine::builtins::MathStep::Complete(Completion::Return(Value::Int(7))), + crate::engine::builtins::MathStep::Complete(Completion::Return(JsValue::Int(7))), &mut |_| panic!("immediate result must not enter waiting sink"), ); assert!(matches!( result, Some(NativeInvokeOutcome::Completion(Completion::Return( - Value::Int(7) + JsValue::Int(7) ))) )); let result = deliver( crate::engine::builtins::ArrayNextStep::Complete( NativeInvokeOutcome::IteratorNextRaw { - value: Value::Int(9), + value: JsValue::Int(9), done: false, }, ), @@ -99,7 +100,7 @@ mod tests { assert!(matches!( result, Some(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Int(9), + value: JsValue::Int(9), done: false }) )); @@ -110,14 +111,13 @@ mod tests { let runtime = Runtime::new(); let mut context = runtime.new_context(); let iterator = context.eval("globalThis.readCount=0;Array.prototype.values.call({get length(){readCount++;return 1;},0:4})").unwrap(); - let step = crate::engine::builtins::ArrayNextStep::start( - &runtime, - context.realm, - &NativeInvocation::Call { - this_value: iterator, - }, - ) - .unwrap(); + let invocation = NativeInvocation::Call { + this_value: runtime.unroot_value(&iterator).unwrap(), + }; + let step = + crate::engine::builtins::ArrayNextStep::start(&runtime, context.realm, &invocation) + .unwrap(); + invocation.release(&runtime).unwrap(); let mut delivered = None; assert!( deliver(step, &mut |step| { diff --git a/src/engine/builtins/date/constructor.rs b/src/engine/builtins/date/constructor.rs index 6e9af976..2659a451 100644 --- a/src/engine/builtins/date/constructor.rs +++ b/src/engine/builtins/date/constructor.rs @@ -56,15 +56,20 @@ impl Runtime { let text = format_date_string(fields.as_ref(), DateStringKind::String).map_err(|_| { RuntimeError::Invariant("the host clock produced an invalid Date string") })?; - Ok(Completion::Return(Value::String(JsString::try_from_utf8( - &text, - )?))) + Ok(Completion::Return(self.unroot_value(&Value::String( + JsString::try_from_utf8(&text)?, + ))?)) } fn call_date_now(&self) -> Result { - Ok(Completion::Return(Value::number( - self.date_now_millis() as f64 - ))) + Ok( + Completion::Return( + crate::engine::value::number::operations::Number::compact( + self.date_now_millis() as f64 + ) + .into(), + ), + ) } fn genuine_date_value(&self, value: &Value) -> Result, RuntimeError> { diff --git a/src/engine/builtins/date/constructor/operation.rs b/src/engine/builtins/date/constructor/operation.rs index a32928d2..d91c8ff3 100644 --- a/src/engine/builtins/date/constructor/operation.rs +++ b/src/engine/builtins/date/constructor/operation.rs @@ -11,7 +11,7 @@ use crate::engine::{ }, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -64,7 +64,7 @@ impl DateConstructorStep { ) -> Result { let new_target = match (kind, invocation) { (DateNativeKind::Constructor, NativeInvocation::Construct { new_target }) => { - new_target.clone() + runtime.root_value(new_target)? } ( DateNativeKind::Now | DateNativeKind::Parse | DateNativeKind::Utc, @@ -83,11 +83,17 @@ impl DateConstructorStep { return Ok(Self::Complete(runtime.call_date_as_function()?)); } let count = arguments.actual_arg_count.min(MAX_DATE_ARGUMENTS); - let values = arguments + let mut values = Vec::new(); + values + .try_reserve_exact(count) + .map_err(|_| RuntimeError::Invariant("Date argv allocation failed"))?; + for value in arguments .readable .get(..count) .ok_or(RuntimeError::Invariant("Date actual arguments unreadable"))? - .to_vec(); + { + values.push(runtime.root_value(value)?); + } let mut resume = DateConstructorResume(Box::new(DateConstructorResumeState { pending_effect: DateConstructorStepPending::default(), realm, @@ -102,18 +108,17 @@ impl DateConstructorStep { if kind == DateNativeKind::Parse { resume.phase = Phase::Parse; return Ok({ - let __pending_field_value = arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined); + let __pending_field_value = match arguments.readable.first() { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, + }; let __pending_field_resume = resume; Self::request_string(__pending_field_value, __pending_field_resume) }); } if arguments.actual_arg_count == 0 { if kind == DateNativeKind::Utc { - return Ok(Self::Complete(Completion::Return(Value::Float(f64::NAN)))); + return Ok(Self::Complete(Completion::Return(JsValue::Float(f64::NAN)))); } resume.value = runtime.date_now_millis() as f64; return resume.prototype(runtime); @@ -129,7 +134,7 @@ impl DateConstructorStep { } resume.phase = Phase::Single; return Ok({ - let __pending_field_value = value; + let __pending_field_value = runtime.into_jsvalue(value)?; let __pending_field_resume = resume; Self::request_primitive(__pending_field_value, __pending_field_resume) }); @@ -147,7 +152,7 @@ impl DateConstructorResume { return Err(RuntimeError::Invariant("Date primitive phase mismatch")); } let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(DateConstructorStep::Complete(Completion::Throw(value))); } @@ -160,7 +165,9 @@ impl DateConstructorResume { match runtime.number_from_primitive(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DateConstructorStep::Complete(Completion::Throw(value))); + return Ok(DateConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } }; @@ -178,19 +185,23 @@ impl DateConstructorResume { let string = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DateConstructorStep::Complete(Completion::Throw(value))); + return Ok(DateConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(DateConstructorStep::Complete(Completion::Return( - Value::number(parsed_date_value(parse_date_string(&string), |instant| { - runtime.date_timezone_offset_minutes(instant) - })), + crate::engine::value::number::operations::Number::compact(parsed_date_value( + parse_date_string(&string), + |instant| runtime.date_timezone_offset_minutes(instant), + )) + .into(), ))) } fn fields(mut self, runtime: &Runtime) -> Result { if let Some(value) = self.0.arguments.next() { return Ok({ - let __pending_field_value = value; + let __pending_field_value = runtime.into_jsvalue(value)?; let __pending_field_resume = self; DateConstructorStep::request_number(__pending_field_value, __pending_field_resume) }); @@ -202,7 +213,7 @@ impl DateConstructorResume { ); if self.0.kind == DateNativeKind::Utc { return Ok(DateConstructorStep::Complete(Completion::Return( - Value::number(self.0.value), + crate::engine::value::number::operations::Number::compact(self.0.value).into(), ))); } self.prototype(runtime) @@ -218,7 +229,9 @@ impl DateConstructorResume { self.0.fields[self.0.index] = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DateConstructorStep::Complete(Completion::Throw(value))); + return Ok(DateConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.index += 1; @@ -227,7 +240,7 @@ impl DateConstructorResume { fn prototype(mut self, runtime: &Runtime) -> Result { self.0.phase = Phase::Prototype; Ok({ - let __pending_field_receiver = self.0.new_target.clone(); + let __pending_field_receiver = runtime.into_jsvalue(self.0.new_target.clone())?; let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?; let __pending_field_resume = self; @@ -248,17 +261,22 @@ impl DateConstructorResume { "Date prototype lookup phase mismatch", )); } - let prototype = match result { - Completion::Return(Value::Object(object)) => object, + let result_value = match result { + Completion::Return(value) => Some(runtime.root_and_release_jsvalue(value)?), Completion::Throw(value) => { return Ok(DateConstructorStep::Complete(Completion::Throw(value))); } - Completion::Return(_) => { + }; + let prototype = match result_value { + Some(Value::Object(object)) => object, + _ => { let realm = match runtime.function_realm_from_value(self.0.realm, &self.0.new_target)? { NativeConversion::Value(realm) => realm, NativeConversion::Throw(value) => { - return Ok(DateConstructorStep::Complete(Completion::Throw(value))); + return Ok(DateConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let prototype = runtime @@ -273,7 +291,11 @@ impl DateConstructorResume { } }; Ok(DateConstructorStep::Complete(Completion::Return( - Value::Object(runtime.new_date_object(&prototype, self.0.value)?), + JsValue::Object( + runtime + .new_date_object(&prototype, self.0.value)? + .into_handle(), + ), ))) } } @@ -289,7 +311,7 @@ pub(crate) fn finish( let value = resume.take_primitive_value(); resume.primitive( runtime, - runtime.to_primitive( + runtime.to_primitive_jsvalue( realm, value, crate::engine::vm::ToPrimitiveHint::Default, @@ -297,15 +319,15 @@ pub(crate) fn finish( )? } DateConstructorStep::String { mut resume } => { - let value = resume.take_string_value(); + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } DateConstructorStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } DateConstructorStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -318,27 +340,27 @@ pub(crate) fn finish( #[derive(Default)] struct DateConstructorStepPending { - primitive_value: Option, - string_value: Option, - number_value: Option, - read_receiver: Option, + primitive_value: Option, + string_value: Option, + number_value: Option, + read_receiver: Option, read_key: Option, } impl DateConstructorStep { - pub(crate) fn request_primitive(value: Value, mut resume: DateConstructorResume) -> Self { + pub(crate) fn request_primitive(value: JsValue, mut resume: DateConstructorResume) -> Self { resume.0.pending_effect.primitive_value = Some(value); Self::Primitive { resume } } - pub(crate) fn request_string(value: Value, mut resume: DateConstructorResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: DateConstructorResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } - pub(crate) fn request_number(value: Value, mut resume: DateConstructorResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: DateConstructorResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: DateConstructorResume, ) -> Self { @@ -348,28 +370,28 @@ impl DateConstructorStep { } } impl DateConstructorResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .pending_effect .primitive_value .take() .expect("DateConstructorStep Primitive value") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value .take() .expect("DateConstructorStep String value") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value .take() .expect("DateConstructorStep Number value") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver diff --git a/src/engine/builtins/date/mod.rs b/src/engine/builtins/date/mod.rs index 264cfd61..66b24b88 100644 --- a/src/engine/builtins/date/mod.rs +++ b/src/engine/builtins/date/mod.rs @@ -75,11 +75,13 @@ impl Runtime { let utc_string_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::ToUTCString)?; let utc_string = match self.get_property_in_realm(realm, date_prototype, &utc_string_key)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) => { - return Err(RuntimeError::Invariant( - "Date.prototype.toUTCString did not materialize as an object", - )); + Completion::Return(value) => { + if !matches!(value, crate::engine::value::JsValue::Object(_)) { + return Err(RuntimeError::Invariant( + "Date.prototype.toUTCString did not materialize as an object", + )); + } + self.root_and_release_jsvalue(value)? } Completion::Throw(_) => { return Err(RuntimeError::Invariant( diff --git a/src/engine/builtins/date/prototype.rs b/src/engine/builtins/date/prototype.rs index f5220ed5..ca7b093d 100644 --- a/src/engine/builtins/date/prototype.rs +++ b/src/engine/builtins/date/prototype.rs @@ -42,10 +42,19 @@ fn date_input_fields(fields: &DateFields) -> DateInputFields { ] } -fn date_argument(arguments: &NativeArguments, index: usize) -> Result<&Value, RuntimeError> { - arguments.readable.get(index).ok_or(RuntimeError::Invariant( - "Date native argument vector was not padded to readable arity", - )) +fn date_argument( + runtime: &Runtime, + arguments: &NativeArguments, + index: usize, +) -> Result { + runtime.root_value( + arguments + .readable + .get(index) + .ok_or(RuntimeError::Invariant( + "Date native argument vector was not padded to readable arity", + ))?, + ) } impl Runtime { @@ -77,19 +86,20 @@ impl Runtime { )); }; + let this_value = self.root_value(this_value)?; match kind { - DateNativeKind::TimeValue => self.call_date_time_value(realm, this_value), - DateNativeKind::String(method) => self.call_date_string(realm, this_value, method), + DateNativeKind::TimeValue => self.call_date_time_value(realm, &this_value), + DateNativeKind::String(method) => self.call_date_string(realm, &this_value, method), DateNativeKind::ToPrimitive => { self.call_date_to_primitive(realm, this_value.clone(), arguments) } - DateNativeKind::TimezoneOffset => self.call_date_timezone_offset(realm, this_value), - DateNativeKind::GetField(field) => self.call_date_get_field(realm, this_value, field), - DateNativeKind::SetTime => self.call_date_set_time(realm, this_value, arguments), + DateNativeKind::TimezoneOffset => self.call_date_timezone_offset(realm, &this_value), + DateNativeKind::GetField(field) => self.call_date_get_field(realm, &this_value, field), + DateNativeKind::SetTime => self.call_date_set_time(realm, &this_value, arguments), DateNativeKind::SetField(field) => { - self.call_date_set_field(realm, this_value, field, arguments) + self.call_date_set_field(realm, &this_value, field, arguments) } - DateNativeKind::SetYear => self.call_date_set_year(realm, this_value, arguments), + DateNativeKind::SetYear => self.call_date_set_year(realm, &this_value, arguments), DateNativeKind::ToJson => self.call_date_to_json(realm, this_value.clone()), DateNativeKind::Constructor | DateNativeKind::Now @@ -172,7 +182,9 @@ impl Runtime { .borrow_mut() .heap .set_date_value(object.object_id(), value)?; - Ok(Completion::Return(Value::number(value))) + Ok(Completion::Return( + crate::engine::value::number::operations::Number::compact(value).into(), + )) } fn call_date_time_value( @@ -182,9 +194,13 @@ impl Runtime { ) -> Result { let (_, value) = match self.date_this_time_value(realm, this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - Ok(Completion::Return(Value::number(value))) + Ok(Completion::Return( + crate::engine::value::number::operations::Number::compact(value).into(), + )) } /// `toGMTString` is not a ninth formatter native. The installer must @@ -199,7 +215,9 @@ impl Runtime { ) -> Result { let (_, value) = match self.date_this_time_value(realm, this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let kind = date_format_kind(method); let fields = get_date_fields(value, kind.uses_local_time(), false, |instant| { @@ -208,16 +226,16 @@ impl Runtime { let output = match format_date_string(fields.as_ref(), kind) { Ok(output) => output, Err(_) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "Date value is NaN", )?)); } }; - Ok(Completion::Return(Value::String(JsString::try_from_utf8( - &output, - )?))) + Ok(Completion::Return(self.unroot_value(&Value::String( + JsString::try_from_utf8(&output)?, + ))?)) } fn call_date_get_field( @@ -228,18 +246,24 @@ impl Runtime { ) -> Result { let (_, value) = match self.date_this_time_value(realm, this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let Some(fields) = get_date_fields(value, field.uses_local_time(), false, |instant| { self.date_timezone_offset_minutes(instant) }) else { - return Ok(Completion::Return(Value::number(f64::NAN))); + return Ok(Completion::Return( + crate::engine::value::number::operations::Number::compact(f64::NAN).into(), + )); }; let mut value = fields[usize::from(field.field_index())]; if field.is_legacy_year() { value -= 1900.0; } - Ok(Completion::Return(Value::number(value))) + Ok(Completion::Return( + crate::engine::value::number::operations::Number::compact(value).into(), + )) } fn call_date_timezone_offset( @@ -249,13 +273,19 @@ impl Runtime { ) -> Result { let (_, value) = match self.date_this_time_value(realm, this_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; if value.is_nan() { - return Ok(Completion::Return(Value::number(f64::NAN))); + return Ok(Completion::Return( + crate::engine::value::number::operations::Number::compact(f64::NAN).into(), + )); } let offset = self.date_timezone_offset_minutes(value.trunc() as i64); - Ok(Completion::Return(Value::number(f64::from(offset)))) + Ok(Completion::Return( + crate::engine::value::number::operations::Number::compact(f64::from(offset)).into(), + )) } fn call_date_set_time( @@ -264,19 +294,20 @@ impl Runtime { this_value: &Value, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::DatePrototypeStep::start( - self, - realm, - DateNativeKind::SetTime, - &NativeInvocation::Call { - this_value: this_value.clone(), - }, - arguments, - )?, - ) + operation::finish(self, realm, { + let invocation = NativeInvocation::Call { + this_value: self.unroot_value(this_value)?, + }; + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::DatePrototypeStep::start( + self, + realm, + DateNativeKind::SetTime, + invocation, + arguments, + ) + })? + }) } fn call_date_set_field( @@ -286,19 +317,20 @@ impl Runtime { field: DateSetFieldKind, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::DatePrototypeStep::start( - self, - realm, - DateNativeKind::SetField(field), - &NativeInvocation::Call { - this_value: this_value.clone(), - }, - arguments, - )?, - ) + operation::finish(self, realm, { + let invocation = NativeInvocation::Call { + this_value: self.unroot_value(this_value)?, + }; + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::DatePrototypeStep::start( + self, + realm, + DateNativeKind::SetField(field), + invocation, + arguments, + ) + })? + }) } fn finish_date_set_year( @@ -338,19 +370,20 @@ impl Runtime { this_value: &Value, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::DatePrototypeStep::start( - self, - realm, - DateNativeKind::SetYear, - &NativeInvocation::Call { - this_value: this_value.clone(), - }, - arguments, - )?, - ) + operation::finish(self, realm, { + let invocation = NativeInvocation::Call { + this_value: self.unroot_value(this_value)?, + }; + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::DatePrototypeStep::start( + self, + realm, + DateNativeKind::SetYear, + invocation, + arguments, + ) + })? + }) } fn call_date_to_primitive( @@ -359,19 +392,20 @@ impl Runtime { this_value: Value, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::DatePrototypeStep::start( - self, - realm, - DateNativeKind::ToPrimitive, - &NativeInvocation::Call { - this_value: this_value.clone(), - }, - arguments, - )?, - ) + operation::finish(self, realm, { + let invocation = NativeInvocation::Call { + this_value: self.unroot_value(&this_value)?, + }; + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::DatePrototypeStep::start( + self, + realm, + DateNativeKind::ToPrimitive, + invocation, + arguments, + ) + })? + }) } fn call_date_to_json( @@ -383,19 +417,20 @@ impl Runtime { readable: Vec::new(), actual_arg_count: 0, }; - operation::finish( - self, - realm, - operation::DatePrototypeStep::start( - self, - realm, - DateNativeKind::ToJson, - &NativeInvocation::Call { - this_value: this_value.clone(), - }, - &arguments, - )?, - ) + operation::finish(self, realm, { + let invocation = NativeInvocation::Call { + this_value: self.unroot_value(&this_value)?, + }; + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::DatePrototypeStep::start( + self, + realm, + DateNativeKind::ToJson, + invocation, + &arguments, + ) + })? + }) } } diff --git a/src/engine/builtins/date/prototype/operation.rs b/src/engine/builtins/date/prototype/operation.rs index e0516602..1f147e23 100644 --- a/src/engine/builtins/date/prototype/operation.rs +++ b/src/engine/builtins/date/prototype/operation.rs @@ -8,7 +8,7 @@ use crate::engine::{ }, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -17,11 +17,11 @@ use crate::engine::{ pub(crate) enum DatePrototypeStep { Complete(Completion), Number { - value: Value, + value: JsValue, resume: DatePrototypeResume, }, Primitive { - value: Value, + value: JsValue, hint: ToPrimitiveHint, resume: DatePrototypeResume, }, @@ -36,7 +36,7 @@ pub(crate) enum DatePrototypeStep { }, Call { callable: CallableRef, - receiver: Value, + receiver: JsValue, }, } enum Phase { @@ -86,27 +86,36 @@ impl DatePrototypeStep { )); }; if kind == DateNativeKind::ToPrimitive { - let Value::Object(object) = this_value else { + let JsValue::Object(id) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not an object", + )?, ))); }; - let hint = match date_argument(arguments, 0)? { + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; + let hint = match date_argument(runtime, arguments, 0)? { Value::String(value) - if value == &JsString::from_static("number") - || value == &JsString::from_static("integer") => + if value == JsString::from_static("number") + || value == JsString::from_static("integer") => { ToPrimitiveHint::Number } Value::String(value) - if value == &JsString::from_static("string") - || value == &JsString::from_static("default") => + if value == JsString::from_static("string") + || value == JsString::from_static("default") => { ToPrimitiveHint::String } _ => { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "invalid hint")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "invalid hint", + )?, ))); } }; @@ -116,14 +125,17 @@ impl DatePrototypeStep { }); } if kind == DateNativeKind::ToJson { - let object = match runtime.native_to_object(realm, this_value.clone())? { - NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); - } - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(this_value)?)? { + NativeConversion::Value(value) => value, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; return Ok(Self::Primitive { - value: Value::Object(object.clone()), + value: JsValue::Object(object.clone().into_handle()), hint: ToPrimitiveHint::Number, resume: DatePrototypeResume(Box::new(DatePrototypeResumeState { realm, @@ -135,9 +147,14 @@ impl DatePrototypeStep { })), }); } - let (object, value) = match runtime.date_this_time_value(realm, this_value)? { + let this_value_value = runtime.root_value(this_value)?; + let (object, value) = match runtime.date_this_time_value(realm, &this_value_value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let (phase, count) = match kind { DateNativeKind::SetTime => (Phase::Time, 1), @@ -166,16 +183,22 @@ impl DatePrototypeStep { )); } }; + let mut arguments_iter = Vec::new(); + arguments_iter + .try_reserve_exact(count) + .map_err(|_| RuntimeError::Invariant("Date setter argv allocation failed"))?; + for value in arguments + .readable + .get(..count) + .ok_or(RuntimeError::Invariant("Date setter argv was not padded"))? + { + arguments_iter.push(runtime.root_value(value)?); + } DatePrototypeResume(Box::new(DatePrototypeResumeState { realm, object: object.clone(), phase, - arguments: arguments - .readable - .get(..count) - .ok_or(RuntimeError::Invariant("Date setter argv was not padded"))? - .to_vec() - .into_iter(), + arguments: arguments_iter.into_iter(), converted: 0, actual: arguments.actual_arg_count, })) @@ -186,7 +209,7 @@ impl DatePrototypeResume { fn next(mut self, runtime: &Runtime) -> Result { if let Some(value) = self.0.arguments.next() { return Ok(DatePrototypeStep::Number { - value, + value: runtime.into_jsvalue(value)?, resume: self, }); } @@ -203,7 +226,7 @@ impl DatePrototypeResume { }; if !had_fields { return Ok(DatePrototypeStep::Complete(Completion::Return( - Value::number(f64::NAN), + crate::engine::value::number::operations::Number::compact(f64::NAN).into(), ))); } let value = if all_finite && self.0.actual > 0 { @@ -227,7 +250,9 @@ impl DatePrototypeResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DatePrototypeStep::Complete(Completion::Throw(value))); + return Ok(DatePrototypeStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match &mut self.0.phase { @@ -259,7 +284,7 @@ impl DatePrototypeResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(DatePrototypeStep::Complete(Completion::Throw(value))); } @@ -267,7 +292,9 @@ impl DatePrototypeResume { match self.0.phase { Phase::JsonPrimitive => { if value.as_number().is_some_and(|value| !value.is_finite()) { - return Ok(DatePrototypeStep::Complete(Completion::Return(Value::Null))); + return Ok(DatePrototypeStep::Complete(Completion::Return( + JsValue::Null, + ))); } self.0.phase = Phase::JsonMethod; Ok(DatePrototypeStep::Read { @@ -285,7 +312,7 @@ impl DatePrototypeResume { }; let Some(callable) = callable else { return Ok(DatePrototypeStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "object needs toISOString method", @@ -294,7 +321,7 @@ impl DatePrototypeResume { }; Ok(DatePrototypeStep::Call { callable, - receiver: Value::Object(self.0.object), + receiver: JsValue::Object(self.0.object.into_handle()), }) } _ => Err(RuntimeError::Invariant( @@ -312,13 +339,14 @@ pub(crate) fn finish( step = match step { DatePrototypeStep::Complete(result) => return Ok(result), DatePrototypeStep::Number { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } DatePrototypeStep::Primitive { value, hint, resume, - } => resume.resume(runtime, runtime.to_primitive(realm, value, hint)?)?, + } => resume.resume(runtime, runtime.to_primitive_jsvalue(realm, value, hint)?)?, DatePrototypeStep::OrdinaryPrimitive { object, hint } => { return runtime.ordinary_to_primitive(realm, &object, hint); } @@ -331,7 +359,12 @@ pub(crate) fn finish( runtime.get_property_in_realm(realm, &object, &key)?, )?, DatePrototypeStep::Call { callable, receiver } => { - return runtime.call_internal(realm, &callable, receiver, &[]); + return runtime.call_internal( + realm, + &callable, + runtime.root_and_release_jsvalue(receiver)?, + &[], + ); } }; } diff --git a/src/engine/builtins/dispatch.rs b/src/engine/builtins/dispatch.rs index 054c8cd8..0dca53db 100644 --- a/src/engine/builtins/dispatch.rs +++ b/src/engine/builtins/dispatch.rs @@ -9,8 +9,8 @@ use crate::engine::heap::ContextId; use crate::engine::object::CallableRef; #[cfg(test)] use crate::engine::value::JsString; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{ @@ -21,8 +21,8 @@ use crate::engine::vm::frames::ActiveFrameKind; // Adapted ABI variants transport the same input owner. Only borrowed inputs // whose variant actually changes need an additional root handle. -fn native_invocation_input(invocation: std::borrow::Cow<'_, NativeInvocation>) -> Value { - match invocation.into_owned() { +fn native_invocation_input(invocation: NativeInvocation) -> crate::engine::value::JsValue { + match invocation { NativeInvocation::Call { this_value } | NativeInvocation::Getter { this_value } | NativeInvocation::Setter { this_value } => this_value, @@ -57,6 +57,37 @@ impl Runtime { Ok(NativeConversion::Value((target, this_argument))) } + /// Internal-value form of [`Runtime::concatenate_bound_arguments`]. The + /// bound roots transfer into internal values without a retain/release pair; + /// the caller's argument edges move into the merged buffer. + pub(crate) fn concatenate_bound_arguments_jsvalue( + &self, + realm: ContextId, + bound_arguments: Vec, + call_arguments: Vec, + ) -> Result>, RuntimeError> { + const MAX_CALL_ARGUMENTS: usize = 65_534; + + let Some(total) = bound_arguments.len().checked_add(call_arguments.len()) else { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Internal, + "stack overflow", + )?)); + }; + if total > MAX_CALL_ARGUMENTS { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Internal, + "stack overflow", + )?)); + } + let mut arguments = Vec::with_capacity(total); + arguments.extend(bound_arguments); + arguments.extend(call_arguments); + Ok(NativeConversion::Value(arguments)) + } + pub(crate) fn concatenate_bound_arguments( &self, realm: ContextId, @@ -137,7 +168,7 @@ impl Runtime { } => { if target == NativeFunctionId::FunctionPrototypeCall { if self.native_call_would_overflow(target) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( caller_realm, NativeErrorKind::Internal, "stack overflow", @@ -177,12 +208,12 @@ impl Runtime { } } NativeConversion::Throw(value) => { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } } } if self.native_call_would_overflow(target) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( caller_realm, NativeErrorKind::Internal, "stack overflow", @@ -207,17 +238,26 @@ impl Runtime { this_value: bound_this, arguments: bound_arguments, } => { + let mut bound_values = Vec::new(); + bound_values + .try_reserve_exact(bound_arguments.len()) + .map_err(|_| RuntimeError::Invariant("bound argument allocation failed"))?; + for value in bound_arguments { + bound_values.push(self.root_and_release_jsvalue(value)?); + } arguments = match self.concatenate_bound_arguments( caller_realm, - &bound_arguments, + &bound_values, &arguments[argument_start..], )? { NativeConversion::Value(arguments) => arguments, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; argument_start = 0; callable = target; - this_value = bound_this; + this_value = self.root_and_release_jsvalue(bound_this)?; } CallableExecution::Proxy => { return self.call_proxy( @@ -246,17 +286,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - super::function::invoke::finish( - self, - realm, - super::function::invoke::InvokeStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + super::function::invoke::finish( self, realm, - super::function::invoke::InvokeKind::Call, - &invocation, - arguments, - )?, - ) + super::function::invoke::InvokeStep::start( + self, + realm, + super::function::invoke::InvokeKind::Call, + invocation, + arguments, + )?, + ) + }) } /// Validate the active native frame and adapt the public call shape to the @@ -269,45 +311,26 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - match self.adapt_native_invocation_input( - target, - realm, - std::borrow::Cow::Owned(invocation), - arguments, - )? { - NativeInvocationAdaptation::Invoke(invocation) => { - Ok(NativeInvocationAdaptation::Invoke(invocation.into_owned())) - } - NativeInvocationAdaptation::Complete(completion) => { - Ok(NativeInvocationAdaptation::Complete(completion)) - } - } + self.adapt_native_invocation_input(target, realm, invocation, arguments) } - pub(crate) fn adapt_native_invocation_borrowed<'a>( + pub(crate) fn adapt_native_invocation_borrowed( &self, target: NativeFunctionId, realm: ContextId, - invocation: &'a NativeInvocation, + invocation: &NativeInvocation, arguments: &NativeArguments, - ) -> Result>, RuntimeError> - { - self.adapt_native_invocation_input( - target, - realm, - std::borrow::Cow::Borrowed(invocation), - arguments, - ) + ) -> Result { + self.adapt_native_invocation_input(target, realm, invocation.dup(self)?, arguments) } - fn adapt_native_invocation_input<'a>( + fn adapt_native_invocation_input( &self, target: NativeFunctionId, realm: ContextId, - invocation: std::borrow::Cow<'a, NativeInvocation>, + invocation: NativeInvocation, arguments: &NativeArguments, - ) -> Result>, RuntimeError> - { + ) -> Result { let frame = self.0 .state @@ -339,20 +362,20 @@ impl Runtime { } // Some handlers do not inspect their adapted this/new-target input, // but keeping it rooted for the full dispatch is part of the ABI. - let invocation = match (target.descriptor().cproto, invocation.as_ref()) { + let invocation = match (target.descriptor().cproto, invocation) { ( NativeCProto::Generic | NativeCProto::GenericMagic | NativeCProto::UnaryF64 | NativeCProto::BinaryF64, - NativeInvocation::Call { .. }, + invocation @ NativeInvocation::Call { .. }, ) => invocation, ( NativeCProto::Generic | NativeCProto::GenericMagic | NativeCProto::UnaryF64 | NativeCProto::BinaryF64, - NativeInvocation::Construct { .. }, + invocation @ NativeInvocation::Construct { .. }, ) => { // QuickJS's generic and floating-point ABIs receive // new.target in their receiver slot when an embedding @@ -360,20 +383,23 @@ impl Runtime { // function object. Floating-point argument conversion stays // in the handler so abrupt completions keep their defining // realm and left-to-right order. - std::borrow::Cow::Owned(NativeInvocation::Call { + NativeInvocation::Call { this_value: native_invocation_input(invocation), - }) + } } ( NativeCProto::Constructor | NativeCProto::ConstructorMagic, - NativeInvocation::Construct { .. }, + invocation @ NativeInvocation::Construct { .. }, ) => invocation, ( NativeCProto::Constructor | NativeCProto::ConstructorMagic, NativeInvocation::Call { .. }, ) => { - let exception = - self.new_native_error(realm, NativeErrorKind::Type, "must be called with new")?; + let exception = self.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "must be called with new", + )?; return Ok(NativeInvocationAdaptation::Complete(Completion::Throw( exception, ))); @@ -381,43 +407,33 @@ impl Runtime { ( NativeCProto::ConstructorOrFunction | NativeCProto::ConstructorOrFunctionMagic, NativeInvocation::Call { .. }, - ) => std::borrow::Cow::Owned(NativeInvocation::Construct { - new_target: Value::Undefined, - }), + ) => NativeInvocation::Construct { + new_target: crate::engine::value::JsValue::Undefined, + }, ( NativeCProto::ConstructorOrFunction | NativeCProto::ConstructorOrFunctionMagic, - NativeInvocation::Construct { .. }, + invocation @ NativeInvocation::Construct { .. }, ) => invocation, - (NativeCProto::Getter | NativeCProto::GetterMagic, NativeInvocation::Call { .. }) => { - std::borrow::Cow::Owned(NativeInvocation::Getter { - this_value: native_invocation_input(invocation), - }) - } ( NativeCProto::Getter | NativeCProto::GetterMagic, - NativeInvocation::Construct { .. }, - ) => std::borrow::Cow::Owned(NativeInvocation::Getter { + invocation @ (NativeInvocation::Call { .. } | NativeInvocation::Construct { .. }), + ) => NativeInvocation::Getter { this_value: native_invocation_input(invocation), - }), - (NativeCProto::Setter | NativeCProto::SetterMagic, NativeInvocation::Call { .. }) => { - std::borrow::Cow::Owned(NativeInvocation::Setter { - this_value: native_invocation_input(invocation), - }) - } + }, ( NativeCProto::Setter | NativeCProto::SetterMagic, - NativeInvocation::Construct { .. }, - ) => std::borrow::Cow::Owned(NativeInvocation::Setter { + invocation @ (NativeInvocation::Call { .. } | NativeInvocation::Construct { .. }), + ) => NativeInvocation::Setter { this_value: native_invocation_input(invocation), - }), - (NativeCProto::IteratorNext, NativeInvocation::Call { .. }) => invocation, - (NativeCProto::IteratorNext, NativeInvocation::Construct { .. }) => { + }, + (NativeCProto::IteratorNext, invocation @ NativeInvocation::Call { .. }) => invocation, + (NativeCProto::IteratorNext, invocation @ NativeInvocation::Construct { .. }) => { // Iterator-next functions are non-constructors by default. // If an embedder independently enables [[Construct]], QuickJS // passes new.target through the same native receiver slot. - std::borrow::Cow::Owned(NativeInvocation::Call { + NativeInvocation::Call { this_value: native_invocation_input(invocation), - }) + } } (_, NativeInvocation::Getter { .. } | NativeInvocation::Setter { .. }) => { return Err(RuntimeError::Invariant( @@ -492,7 +508,7 @@ impl Runtime { TypedArrayNativeKind as Ta, }; match target { - NativeFunctionId::FunctionPrototype => Ok(Completion::Return(Value::Undefined)), + NativeFunctionId::FunctionPrototype => Ok(Completion::Return(JsValue::Undefined)), NativeFunctionId::Map(kind) => { self.call_map_native_borrowed(realm, kind, invocation, arguments) } @@ -528,7 +544,7 @@ impl Runtime { ) => self.call_shared_array_buffer_getter(realm, kind, invocation), NativeFunctionId::DataView(kind) => self.call_data_view_getter(realm, kind, invocation), NativeFunctionId::TypedArray(Ta::BaseConstructor) => { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, crate::engine::api::error::NativeErrorKind::Type, "cannot be called", @@ -607,32 +623,32 @@ impl Runtime { NativeFunctionId::ArgumentProbe | NativeFunctionId::ConstructorProbe | NativeFunctionId::ConstructorOrFunctionProbe => { - if matches!(arguments.readable.first(), Some(Value::Bool(false))) { - return Ok(Completion::Throw(Value::String(JsString::from_static( - "native probe throw", - )))); + if matches!(arguments.readable.first(), Some(JsValue::Bool(false))) { + return Ok(Completion::Throw(self.unroot_value(&Value::String( + JsString::from_static("native probe throw"), + ))?)); } - if matches!(arguments.readable.first(), Some(Value::Bool(true))) { + if matches!(arguments.readable.first(), Some(JsValue::Bool(true))) { return Err(RuntimeError::Invariant("native probe engine error")); } let padded_undefined = arguments.readable[arguments.actual_arg_count..] .iter() - .filter(|value| matches!(value, Value::Undefined)) + .filter(|value| matches!(value, JsValue::Undefined)) .count(); let active_function = self.active_function()?.object_id(); let invocation_target_is_function = match invocation { NativeInvocation::Call { - this_value: Value::Object(object), - } => object.object_id() == active_function, + this_value: JsValue::Object(object), + } => *object == active_function, NativeInvocation::Construct { - new_target: Value::Object(object), - } => object.object_id() == active_function, + new_target: JsValue::Object(object), + } => *object == active_function, NativeInvocation::Getter { - this_value: Value::Object(object), - } => object.object_id() == active_function, + this_value: JsValue::Object(object), + } => *object == active_function, NativeInvocation::Setter { - this_value: Value::Object(object), - } => object.object_id() == active_function, + this_value: JsValue::Object(object), + } => *object == active_function, NativeInvocation::Call { .. } | NativeInvocation::Construct { .. } | NativeInvocation::Getter { .. } @@ -645,15 +661,30 @@ impl Runtime { padded_undefined, invocation_target_is_function ); - Ok(Completion::Return(Value::String(JsString::try_from_utf8( - &result, - )?))) + Ok(Completion::Return(self.unroot_value(&Value::String( + JsString::try_from_utf8(&result)?, + ))?)) } _ => Err(RuntimeError::Invariant( "unregistered synchronous native leaf", )), } } + /// Run one handler that only borrows its invocation, then release the + /// owned invocation edge. A handler error keeps precedence over a release + /// error, and the edge is released on both the success and error paths. + pub(crate) fn dispatch_borrowed_invocation( + &self, + invocation: NativeInvocation, + handler: impl FnOnce(&NativeInvocation) -> Result, + ) -> Result { + let result = handler(&invocation); + match (result, invocation.release(self)) { + (Ok(value), Ok(())) => Ok(value), + (Err(error), _) => Err(error), + (Ok(_), Err(error)) => Err(error), + } + } pub(crate) fn dispatch_adapted_native_function( &self, callable: &crate::engine::object::CallableRef, @@ -663,7 +694,7 @@ impl Runtime { arguments: &NativeArguments, ) -> Result { match target { - NativeFunctionId::FunctionPrototype => Ok(Completion::Return(Value::Undefined)), + NativeFunctionId::FunctionPrototype => Ok(Completion::Return(JsValue::Undefined)), NativeFunctionId::FunctionConstructor(kind) => { self.call_function_constructor(realm, kind, invocation, arguments) } @@ -676,9 +707,10 @@ impl Runtime { NativeFunctionId::ArrayConstructor => { self.call_array_constructor(realm, invocation, arguments) } - NativeFunctionId::ArrayIsArray => { - self.call_array_is_array(realm, &invocation, arguments) - } + NativeFunctionId::ArrayIsArray => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_array_is_array(realm, invocation, arguments) + }), NativeFunctionId::ArrayFrom => self.call_array_from(realm, invocation, arguments), NativeFunctionId::ArrayOf => self.call_array_of(realm, invocation, arguments), NativeFunctionId::ArraySpeciesGetter => self.call_array_species_getter(invocation), @@ -742,9 +774,10 @@ impl Runtime { NativeFunctionId::ArrayPrototypeToSpliced => { self.call_array_prototype_to_spliced(realm, invocation, arguments) } - NativeFunctionId::ArrayPrototypeIterator(kind) => { - self.call_array_prototype_iterator(realm, kind, &invocation) - } + NativeFunctionId::ArrayPrototypeIterator(kind) => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_array_prototype_iterator(realm, kind, invocation) + }), NativeFunctionId::ArrayIteratorNext => self.call_array_iterator_next(realm, invocation), NativeFunctionId::Map(kind) => self.call_map_native(realm, kind, invocation, arguments), NativeFunctionId::MapIteratorNext => self.call_map_iterator_next(realm, invocation), @@ -756,11 +789,14 @@ impl Runtime { NativeFunctionId::WeakSet(kind) => { self.call_weak_set_native(realm, kind, invocation, arguments) } - NativeFunctionId::WeakRef(kind) => { - self.call_weak_ref_native(realm, kind, &invocation, arguments) - } + NativeFunctionId::WeakRef(kind) => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_weak_ref_native(realm, kind, invocation, arguments) + }), NativeFunctionId::FinalizationRegistry(kind) => { - self.call_finalization_registry_native(realm, kind, &invocation, arguments) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.call_finalization_registry_native(realm, kind, invocation, arguments) + }) } NativeFunctionId::ArrayBuffer(kind) => { self.call_array_buffer_native(realm, kind, invocation, arguments) @@ -822,9 +858,10 @@ impl Runtime { NativeFunctionId::DynamicImportHandler(kind) => { self.call_dynamic_import_handler(realm, kind, invocation, arguments) } - NativeFunctionId::ThrowTypeError => { - self.call_throw_type_error(realm, &invocation, arguments) - } + NativeFunctionId::ThrowTypeError => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_throw_type_error(realm, invocation, arguments) + }), NativeFunctionId::FunctionPrototypeCall => { self.call_function_prototype_call(realm, invocation, arguments) } @@ -840,12 +877,14 @@ impl Runtime { NativeFunctionId::FunctionPrototypeHasInstance => { self.call_function_prototype_has_instance(realm, invocation, arguments) } - NativeFunctionId::FunctionPrototypeFileName => { - self.call_function_prototype_file_name(&invocation) - } - NativeFunctionId::FunctionPrototypePosition(selector) => { - self.call_function_prototype_position(&invocation, selector) - } + NativeFunctionId::FunctionPrototypeFileName => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_function_prototype_file_name(invocation) + }), + NativeFunctionId::FunctionPrototypePosition(selector) => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_function_prototype_position(invocation, selector) + }), NativeFunctionId::ObjectConstructor => { self.call_object_constructor(realm, invocation, arguments) } @@ -944,19 +983,29 @@ impl Runtime { self.call_string_code_point_range(realm, invocation, arguments) } #[cfg(feature = "test262-host")] - NativeFunctionId::Test262DetachArrayBuffer => { - self.call_test262_detach_array_buffer(&invocation, arguments) - } + NativeFunctionId::Test262DetachArrayBuffer => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_test262_detach_array_buffer(invocation, arguments) + }), #[cfg(feature = "test262-host")] NativeFunctionId::Test262EvalScript => { self.call_test262_eval_script(realm, invocation, arguments) } #[cfg(feature = "test262-host")] - NativeFunctionId::Test262CreateRealm => self.call_test262_create_realm(&invocation), + NativeFunctionId::Test262CreateRealm => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_test262_create_realm(invocation) + }), #[cfg(feature = "test262-host")] - NativeFunctionId::Test262IsHtmlDda => self.call_test262_is_html_dda(&invocation), + NativeFunctionId::Test262IsHtmlDda => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_test262_is_html_dda(invocation) + }), #[cfg(feature = "test262-host")] - NativeFunctionId::Test262Gc => self.call_test262_gc(&invocation), + NativeFunctionId::Test262Gc => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_test262_gc(invocation) + }), #[cfg(feature = "test262-host")] NativeFunctionId::Test262Agent(kind) => { self.call_test262_agent(realm, kind, invocation, arguments) @@ -967,9 +1016,10 @@ impl Runtime { NativeFunctionId::PrimitivePrototypeToString(kind) => { self.call_primitive_prototype_to_string(realm, kind, invocation, arguments) } - NativeFunctionId::PrimitivePrototypeValueOf(kind) => { - self.call_primitive_prototype_value_of(realm, kind, &invocation) - } + NativeFunctionId::PrimitivePrototypeValueOf(kind) => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_primitive_prototype_value_of(realm, kind, invocation) + }), NativeFunctionId::StringPrototypeCharAt(selector) => { self.call_string_prototype_char_at(realm, selector, invocation, arguments) } @@ -1016,7 +1066,10 @@ impl Runtime { self.call_math_binary(realm, kind, invocation, arguments) } NativeFunctionId::MathHypot => self.call_math_hypot(realm, invocation, arguments), - NativeFunctionId::MathRandom => self.call_math_random(realm, &invocation), + NativeFunctionId::MathRandom => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_math_random(realm, invocation) + }), NativeFunctionId::MathImul => self.call_math_imul(realm, invocation, arguments), NativeFunctionId::MathClz32 => self.call_math_clz32(realm, invocation, arguments), NativeFunctionId::MathSumPrecise => { @@ -1080,12 +1133,14 @@ impl Runtime { NativeFunctionId::IteratorConcatReturn => { self.call_iterator_concat_return(realm, invocation) } - NativeFunctionId::IteratorPrototypeIterator => { - self.call_iterator_prototype_iterator(&invocation) - } - NativeFunctionId::IteratorPrototypeToStringTagGetter => { - self.call_iterator_prototype_to_string_tag_getter(&invocation) - } + NativeFunctionId::IteratorPrototypeIterator => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_iterator_prototype_iterator(invocation) + }), + NativeFunctionId::IteratorPrototypeToStringTagGetter => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_iterator_prototype_to_string_tag_getter(invocation) + }), NativeFunctionId::IteratorPrototypeToStringTagSetter => { self.call_iterator_prototype_to_string_tag_setter(realm, invocation, arguments) } @@ -1098,12 +1153,14 @@ impl Runtime { NativeFunctionId::RegExpStringIteratorNext => { self.call_regexp_string_iterator_next(realm, invocation) } - NativeFunctionId::SymbolRegistry(kind) => { - self.call_symbol_registry(realm, kind, &invocation, arguments) - } - NativeFunctionId::SymbolPrototypeDescription => { - self.call_symbol_prototype_description(realm, &invocation) - } + NativeFunctionId::SymbolRegistry(kind) => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_symbol_registry(realm, kind, invocation, arguments) + }), + NativeFunctionId::SymbolPrototypeDescription => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_symbol_prototype_description(realm, invocation) + }), NativeFunctionId::BigIntAsN(kind) => { self.call_bigint_as_n(realm, kind, invocation, arguments) } @@ -1117,9 +1174,10 @@ impl Runtime { NativeFunctionId::GlobalUriCodec(kind) => { self.call_global_uri_codec(realm, kind, invocation, arguments) } - NativeFunctionId::NumberPredicate(kind) => { - self.call_number_predicate(kind, &invocation, arguments) - } + NativeFunctionId::NumberPredicate(kind) => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_number_predicate(kind, invocation, arguments) + }), NativeFunctionId::NumberPrototypeFormat(kind) => { self.call_number_prototype_format(realm, kind, invocation, arguments) } @@ -1143,32 +1201,32 @@ impl Runtime { NativeFunctionId::ArgumentProbe | NativeFunctionId::ConstructorProbe | NativeFunctionId::ConstructorOrFunctionProbe => { - if matches!(arguments.readable.first(), Some(Value::Bool(false))) { - return Ok(Completion::Throw(Value::String(JsString::from_static( - "native probe throw", - )))); + if matches!(arguments.readable.first(), Some(JsValue::Bool(false))) { + return Ok(Completion::Throw(self.unroot_value(&Value::String( + JsString::from_static("native probe throw"), + ))?)); } - if matches!(arguments.readable.first(), Some(Value::Bool(true))) { + if matches!(arguments.readable.first(), Some(JsValue::Bool(true))) { return Err(RuntimeError::Invariant("native probe engine error")); } let padded_undefined = arguments.readable[arguments.actual_arg_count..] .iter() - .filter(|value| matches!(value, Value::Undefined)) + .filter(|value| matches!(value, JsValue::Undefined)) .count(); let active_function = self.active_function()?.object_id(); let invocation_target_is_function = match invocation { NativeInvocation::Call { - this_value: Value::Object(object), - } => object.object_id() == active_function, + this_value: JsValue::Object(object), + } => object == active_function, NativeInvocation::Construct { - new_target: Value::Object(object), - } => object.object_id() == active_function, + new_target: JsValue::Object(object), + } => object == active_function, NativeInvocation::Getter { - this_value: Value::Object(object), - } => object.object_id() == active_function, + this_value: JsValue::Object(object), + } => object == active_function, NativeInvocation::Setter { - this_value: Value::Object(object), - } => object.object_id() == active_function, + this_value: JsValue::Object(object), + } => object == active_function, NativeInvocation::Call { .. } | NativeInvocation::Construct { .. } | NativeInvocation::Getter { .. } @@ -1181,9 +1239,9 @@ impl Runtime { padded_undefined, invocation_target_is_function ); - Ok(Completion::Return(Value::String(JsString::try_from_utf8( - &result, - )?))) + Ok(Completion::Return(self.unroot_value(&Value::String( + JsString::try_from_utf8(&result)?, + ))?)) } } } diff --git a/src/engine/builtins/error/aggregate.rs b/src/engine/builtins/error/aggregate.rs index 41829c56..4c717ab9 100644 --- a/src/engine/builtins/error/aggregate.rs +++ b/src/engine/builtins/error/aggregate.rs @@ -10,24 +10,24 @@ use crate::engine::{ CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }, - value::Value, + value::{JsValue, Value}, vm::Completion, }; pub(crate) enum AggregateStep { Complete(Completion), Read { - receiver: Value, + receiver: JsValue, key: PropertyKey, resume: AggregateResume, }, Call { callable: CallableRef, - receiver: Value, + receiver: JsValue, resume: AggregateResume, }, Next { iterator: ObjectRef, - next: Value, + next: JsValue, resume: AggregateResume, }, Close { @@ -55,14 +55,30 @@ impl std::ops::DerefMut for AggregateResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct AggregateResumeState { + runtime: Runtime, realm: ContextId, phase: Phase, - iterable: Value, + iterable: JsValue, iterator: Option, - next: Value, + next: JsValue, result: Option, index: u64, } +impl Drop for AggregateResumeState { + /// Release the internal edges still owned when the request is abandoned. + /// Consumption goes through `std::mem::replace` or a duplicate, so drained + /// fields are `Undefined` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if !matches!(self.iterable, JsValue::Undefined) { + let iterable = std::mem::replace(&mut self.iterable, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(iterable); + } + if !matches!(self.next, JsValue::Undefined) { + let next = std::mem::replace(&mut self.next, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(next); + } + } +} impl AggregateStep { pub(crate) fn start( runtime: &Runtime, @@ -71,7 +87,7 @@ impl AggregateStep { ) -> Result { if matches!(iterable, Value::Null | Value::Undefined) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!( @@ -85,15 +101,17 @@ impl AggregateStep { )?, ))); } + let iterable = runtime.into_jsvalue(iterable)?; Ok(Self::Read { - receiver: iterable.clone(), + receiver: runtime.dup_jsvalue(&iterable)?, key: PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), resume: AggregateResume(Box::new(AggregateResumeState { + runtime: runtime.clone(), realm, phase: Phase::Method, iterable, iterator: None, - next: Value::Undefined, + next: JsValue::Undefined, result: None, index: 0, })), @@ -107,7 +125,7 @@ impl AggregateResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(AggregateStep::Complete(Completion::Throw(value))); } @@ -120,7 +138,7 @@ impl AggregateResume { }; let Some(callable) = callable else { return Ok(AggregateStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "value is not iterable", @@ -130,41 +148,42 @@ impl AggregateResume { self.0.phase = Phase::Iterator; Ok(AggregateStep::Call { callable, - receiver: self.0.iterable.clone(), + receiver: runtime.dup_jsvalue(&self.0.iterable)?, resume: self, }) } Phase::Iterator => { let Value::Object(iterator) = value else { return Ok(AggregateStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", )?, ))); }; - self.0.iterable = Value::Undefined; + let iterable = std::mem::replace(&mut self.0.iterable, JsValue::Undefined); + runtime.release_jsvalue(iterable)?; self.0.iterator = Some(iterator.clone()); self.0.phase = Phase::NextMethod; Ok(AggregateStep::Read { - receiver: Value::Object(iterator), + receiver: JsValue::Object(iterator.into_handle()), key: runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?, resume: self, }) } Phase::NextMethod => { - self.0.next = value; + self.0.next = runtime.into_jsvalue(value)?; self.0.result = Some(runtime.new_array(self.0.realm)?); - self.next() + self.next(runtime) } _ => Err(RuntimeError::Invariant( "AggregateError value phase mismatch", )), } } - fn next(mut self) -> Result { + fn next(mut self, runtime: &Runtime) -> Result { self.0.phase = Phase::Next; Ok(AggregateStep::Next { iterator: self @@ -172,15 +191,17 @@ impl AggregateResume { .iterator .clone() .ok_or(RuntimeError::Invariant("AggregateError iterator missing"))?, - next: self.0.next.clone(), + next: runtime.dup_jsvalue(&self.0.next)?, resume: self, }) } - fn close(self, value: Value) -> Result { + fn close(self, runtime: &Runtime, value: JsValue) -> Result { + let _ = runtime; Ok(AggregateStep::Close { iterator: self .0 .iterator + .clone() .ok_or(RuntimeError::Invariant("AggregateError iterator missing"))?, completion: Completion::Throw(value), }) @@ -198,13 +219,16 @@ impl AggregateResume { let value = match result { ObjectIteratorStep::Yield(value) => value, ObjectIteratorStep::Done => { - return Ok(AggregateStep::Complete(Completion::Return(Value::Object( - self.0 - .result - .ok_or(RuntimeError::Invariant("AggregateError result missing"))?, - )))); + let result = self + .0 + .result + .clone() + .ok_or(RuntimeError::Invariant("AggregateError result missing"))?; + return Ok(AggregateStep::Complete(Completion::Return( + JsValue::Object(result.into_handle()), + ))); } - ObjectIteratorStep::Throw(value) => return self.close(value), + ObjectIteratorStep::Throw(value) => return self.close(runtime, value), }; let result = self .0 @@ -212,6 +236,7 @@ impl AggregateResume { .as_ref() .ok_or(RuntimeError::Invariant("AggregateError result missing"))?; let key = runtime.intern_property_key(&self.0.index.to_string())?; + let value = runtime.root_and_release_jsvalue(value)?; // The result has not been exposed to JavaScript: own data definition on this fresh Array is callback-free. match runtime.define_own_property( result, @@ -234,7 +259,7 @@ impl AggregateResume { self.0.index = self.0.index.checked_add(1).ok_or(RuntimeError::Invariant( "AggregateError iterable exceeded Uint64 indices", ))?; - self.next() + self.next(runtime) } } pub(crate) fn finish( @@ -251,7 +276,11 @@ pub(crate) fn finish( resume, } => resume.resume( runtime, - runtime.get_value_property_in_realm(realm, receiver, &key)?, + runtime.get_value_property_in_realm( + realm, + runtime.root_and_release_jsvalue(receiver)?, + &key, + )?, )?, AggregateStep::Call { callable, @@ -259,7 +288,12 @@ pub(crate) fn finish( resume, } => resume.resume( runtime, - runtime.call_internal(realm, &callable, receiver, &[])?, + runtime.call_internal( + realm, + &callable, + runtime.root_and_release_jsvalue(receiver)?, + &[], + )?, )?, AggregateStep::Next { iterator, @@ -270,7 +304,12 @@ pub(crate) fn finish( finish_next( runtime, realm, - NextStep::start(runtime, realm, iterator, next)?, + NextStep::start( + runtime, + realm, + iterator, + runtime.root_and_release_jsvalue(next)?, + )?, )?, )?, AggregateStep::Close { diff --git a/src/engine/builtins/error/backtrace.rs b/src/engine/builtins/error/backtrace.rs index 1d0ff0e2..56a89ccc 100644 --- a/src/engine/builtins/error/backtrace.rs +++ b/src/engine/builtins/error/backtrace.rs @@ -1,7 +1,7 @@ use crate::engine::api::error::ErrorKind; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::heap::ObjectPayload; use crate::engine::object::access::raw_string_property_one_level; @@ -28,18 +28,39 @@ impl Runtime { if !object.belongs_to(self) { return Err(RuntimeError::WrongRuntime("backtrace Error object")); } + self.ensure_error_backtrace_object(object.object_id(), skip_first_frame, explicit_location) + } + /// Internal-value form of [`Runtime::ensure_error_backtrace`]. + pub(crate) fn ensure_error_backtrace_jsvalue( + &self, + value: &crate::engine::value::JsValue, + skip_first_frame: bool, + explicit_location: Option, + ) -> Result<(), RuntimeError> { + let crate::engine::value::JsValue::Object(object) = value else { + return Ok(()); + }; + self.ensure_error_backtrace_object(*object, skip_first_frame, explicit_location) + } + + fn ensure_error_backtrace_object( + &self, + object: crate::engine::heap::ObjectId, + skip_first_frame: bool, + explicit_location: Option, + ) -> Result<(), RuntimeError> { let stack_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Stack)?; let needs_backtrace = { let state = self.0.state.borrow(); - let data = state.heap.object(object.object_id())?; + let data = state.heap.object(object)?; if !matches!(data.payload, ObjectPayload::Error) { false } else { state .heap .shape(data.shape)? - .find(stack_key.atom()) + .find(AtomIdx::from_raw(stack_key.atom().raw())) .is_none() } }; @@ -66,6 +87,7 @@ impl Runtime { Err(error) => return Err(error), }; + let object_ref = ObjectRef::from_borrowed_handle(self.clone(), object)?; // Parse errors add SpiderMonkey-compatible metadata before `stack`, // exactly as QuickJS does. Rejection (for example after // preventExtensions) is intentionally silent: build_backtrace must @@ -87,13 +109,13 @@ impl Runtime { ("lineNumber", Value::Int(line)), ("columnNumber", Value::Int(column)), ] { - if !self.define_backtrace_property(object, name, property_value)? { + if !self.define_backtrace_property(&object_ref, name, property_value)? { return Ok(()); } } } - let _ = self.define_backtrace_property(object, "stack", Value::String(stack))?; + let _ = self.define_backtrace_property(&object_ref, "stack", Value::String(stack))?; Ok(()) } diff --git a/src/engine/builtins/error/construction.rs b/src/engine/builtins/error/construction.rs index 070ec62e..784ee845 100644 --- a/src/engine/builtins/error/construction.rs +++ b/src/engine/builtins/error/construction.rs @@ -4,10 +4,25 @@ use crate::engine::api::runtime_error::RuntimeError; use crate::engine::heap::{ContextId, ObjectData, ObjectKind}; use crate::engine::object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::frames::ActiveFrameKind; impl Runtime { + /// Internal-value form of native Error construction: the returned value + /// owns the new error object's single edge. + pub(crate) fn new_native_error_jsvalue( + &self, + realm: ContextId, + kind: NativeErrorKind, + message: &str, + ) -> Result { + self.new_native_error_from_message_jsvalue( + realm, + kind, + NativeErrorMessage::from_utf8(message), + ) + } + pub(crate) fn new_native_error( &self, realm: ContextId, @@ -17,6 +32,20 @@ impl Runtime { self.new_native_error_from_message(realm, kind, NativeErrorMessage::from_utf8(message)) } + /// Internal-value form of [`Runtime::new_native_error_from_error`]. + pub(crate) fn new_native_error_from_error_jsvalue( + &self, + realm: ContextId, + kind: NativeErrorKind, + error: &Error, + ) -> Result { + let message = error + .native_message() + .cloned() + .unwrap_or_else(|| NativeErrorMessage::from_utf8(error.message())); + self.new_native_error_from_message_jsvalue(realm, kind, message) + } + pub(crate) fn new_native_error_from_error( &self, realm: ContextId, @@ -30,6 +59,33 @@ impl Runtime { self.new_native_error_from_message(realm, kind, message) } + /// Internal-value form of [`Runtime::new_native_error_from_message`]. + pub(crate) fn new_native_error_from_message_jsvalue( + &self, + realm: ContextId, + kind: NativeErrorKind, + message: NativeErrorMessage, + ) -> Result { + let value = + self.new_native_error_without_backtrace_from_message_jsvalue(realm, kind, message)?; + let capture_now = self + .0 + .state + .borrow() + .active_frames + .last() + .is_none_or(|frame| matches!(frame.kind, ActiveFrameKind::Native { .. })); + if capture_now { + let JsValue::Object(_) = &value else { + return Err(RuntimeError::Invariant( + "native Error construction did not produce an object", + )); + }; + self.ensure_error_backtrace_jsvalue(&value, false, None)?; + } + Ok(value) + } + pub(crate) fn new_native_error_from_message( &self, realm: ContextId, @@ -50,9 +106,6 @@ impl Runtime { Ok(value) } - /// `JS_ThrowError2(..., add_backtrace = FALSE)` construction path used by - /// parser diagnostics, which prepend their explicit filename location - /// before adding the active frame chain. pub(crate) fn new_native_error_without_backtrace_from_error( &self, realm: ContextId, @@ -66,6 +119,42 @@ impl Runtime { self.new_native_error_without_backtrace_from_message(realm, kind, message) } + /// Internal-value form of + /// [`Runtime::new_native_error_without_backtrace_from_message`]. + pub(crate) fn new_native_error_without_backtrace_from_message_jsvalue( + &self, + realm: ContextId, + kind: NativeErrorKind, + message: NativeErrorMessage, + ) -> Result { + let prototype = { + let state = self.0.state.borrow(); + state.heap.context(realm)?.native_error_prototypes[kind.index()].ok_or( + RuntimeError::Invariant("realm has no native Error prototype"), + )? + }; + let prototype = ObjectRef::from_borrowed_handle(self.clone(), prototype)?; + let object = self.new_error_object(&prototype)?; + let key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Message)?; + let defined = self.define_own_property( + &object, + &key, + &OrdinaryPropertyDescriptor { + value: DescriptorField::Present(Value::String(message.to_js_string()?)), + writable: DescriptorField::Present(true), + enumerable: DescriptorField::Present(false), + configurable: DescriptorField::Present(true), + ..OrdinaryPropertyDescriptor::new() + }, + )?; + if !defined { + return Err(RuntimeError::Invariant( + "native Error message definition was rejected", + )); + } + Ok(JsValue::Object(object.into_handle())) + } + pub(crate) fn new_native_error_without_backtrace_from_message( &self, realm: ContextId, diff --git a/src/engine/builtins/error/mod.rs b/src/engine/builtins/error/mod.rs index e3e42bb1..874f25d7 100644 --- a/src/engine/builtins/error/mod.rs +++ b/src/engine/builtins/error/mod.rs @@ -110,17 +110,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::ErrorStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::ErrorKind::Constructor(kind), - &invocation, - arguments, - )?, - ) + operation::ErrorStep::start( + self, + realm, + operation::ErrorKind::Constructor(kind), + invocation, + arguments, + )?, + ) + }) } /// Pinned QuickJS's internal `Promise.any` AggregateError path: retain the @@ -151,21 +153,23 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - let arguments = NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }; - operation::finish( - self, - realm, - operation::ErrorStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + let arguments = NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }; + operation::finish( self, realm, - operation::ErrorKind::ToString, - &invocation, - &arguments, - )?, - ) + operation::ErrorStep::start( + self, + realm, + operation::ErrorKind::ToString, + invocation, + &arguments, + )?, + ) + }) } pub(crate) fn call_error_is_error( @@ -176,16 +180,21 @@ impl Runtime { "Error.isError readable argv was not padded to length one", ))?; let is_error = match value { - Value::Object(object) => self.is_error_object(object)?, - Value::Undefined - | Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_) - | Value::BigInt(_) - | Value::String(_) - | Value::Symbol(_) => false, + crate::engine::value::JsValue::Object(id) => { + let object = ObjectRef::from_borrowed_handle(self.clone(), *id)?; + self.is_error_object(&object)? + } + crate::engine::value::JsValue::Undefined + | crate::engine::value::JsValue::Null + | crate::engine::value::JsValue::Bool(_) + | crate::engine::value::JsValue::Int(_) + | crate::engine::value::JsValue::Float(_) + | crate::engine::value::JsValue::BigInt(_) + | crate::engine::value::JsValue::String(_) + | crate::engine::value::JsValue::Symbol(_) => false, }; - Ok(Completion::Return(Value::Bool(is_error))) + Ok(Completion::Return(crate::engine::value::JsValue::Bool( + is_error, + ))) } } diff --git a/src/engine/builtins/error/operation.rs b/src/engine/builtins/error/operation.rs index dbc87f6b..9d4e4e4f 100644 --- a/src/engine/builtins/error/operation.rs +++ b/src/engine/builtins/error/operation.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::ErrorConstructorKind, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -76,6 +76,13 @@ impl ErrorStep { invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { + let mut owned_arguments = Vec::new(); + owned_arguments + .try_reserve_exact(arguments.readable.len()) + .map_err(|_| RuntimeError::Invariant("Error argument allocation failed"))?; + for argument in &arguments.readable { + owned_arguments.push(runtime.root_value(argument)?); + } let mut resume = ErrorResume(Box::new(ErrorResumeState { pending_effect: ErrorStepPending::default(), realm, @@ -83,7 +90,7 @@ impl ErrorStep { phase: Phase::Prototype, object: None, new_target: Value::Undefined, - arguments: arguments.readable.clone(), + arguments: owned_arguments, actual: arguments.actual_arg_count, name: JsString::from_static("Error"), })); @@ -94,13 +101,14 @@ impl ErrorStep { "Error constructor requires constructor-or-function invocation", )); }; - resume.new_target = if matches!(new_target, Value::Undefined) { + resume.new_target = if matches!(new_target, JsValue::Undefined) { Value::Object(runtime.active_function()?) } else { - new_target.clone() + runtime.root_value(new_target)? }; Ok({ - let __pending_field_receiver = resume.new_target.clone(); + let __pending_field_receiver = + runtime.into_jsvalue(resume.new_target.clone())?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?; let __pending_field_resume = resume; @@ -117,15 +125,20 @@ impl ErrorStep { "Error string requires generic invocation", )); }; - let Value::Object(object) = this_value else { + let JsValue::Object(object) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not an object", + )?, ))); }; + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *object)?; resume.object = Some(object.clone()); resume.phase = Phase::NameRead; Ok({ - let __pending_field_receiver = this_value.clone(); + let __pending_field_receiver = JsValue::Object(object.into_handle()); let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Name)?; let __pending_field_resume = resume; @@ -158,7 +171,7 @@ impl ErrorResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(ErrorStep::Complete(Completion::Throw(value))), }; match self.0.phase { @@ -174,7 +187,9 @@ impl ErrorResume { { NativeConversion::Value(realm) => realm, NativeConversion::Throw(value) => { - return Ok(ErrorStep::Complete(Completion::Throw(value))); + return Ok(ErrorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let prototype = { @@ -205,7 +220,7 @@ impl ErrorResume { } else { self.0.phase = Phase::Message; Ok({ - let __pending_field_value = message; + let __pending_field_value = runtime.into_jsvalue(message)?; let __pending_field_resume = self; ErrorStep::request_string(__pending_field_value, __pending_field_resume) }) @@ -242,7 +257,7 @@ impl ErrorResume { } else { self.0.phase = Phase::Name; Ok({ - let __pending_field_value = value; + let __pending_field_value = runtime.into_jsvalue(value)?; let __pending_field_resume = self; ErrorStep::request_string(__pending_field_value, __pending_field_resume) }) @@ -254,7 +269,7 @@ impl ErrorResume { self.string(runtime, NativeConversion::Value(JsString::from_static(""))) } else { Ok({ - let __pending_field_value = value; + let __pending_field_value = runtime.into_jsvalue(value)?; let __pending_field_resume = self; ErrorStep::request_string(__pending_field_value, __pending_field_resume) }) @@ -271,7 +286,9 @@ impl ErrorResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ErrorStep::Complete(Completion::Throw(value))); + return Ok(ErrorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -300,9 +317,9 @@ impl ErrorResume { .try_concat(&JsString::from_static(": "))? .try_concat(&value)? }; - Ok(ErrorStep::Complete(Completion::Return(Value::String( - value, - )))) + Ok(ErrorStep::Complete(Completion::Return( + runtime.unroot_value(&Value::String(value))?, + ))) } _ => Err(RuntimeError::Invariant("Error string reply phase mismatch")), } @@ -310,7 +327,7 @@ impl ErrorResume { fn text(mut self, runtime: &Runtime) -> Result { self.0.phase = Phase::TextRead; Ok({ - let __pending_field_receiver = Value::Object(self.object()?); + let __pending_field_receiver = JsValue::Object(self.object()?.into_handle()); let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Message)?; let __pending_field_resume = self; @@ -355,7 +372,9 @@ impl ErrorResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ErrorStep::Complete(Completion::Throw(value))); + return Ok(ErrorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !value { @@ -364,7 +383,7 @@ impl ErrorResume { let receiver = self.0.arguments[usize::from(self.aggregate_kind()) + 1].clone(); self.0.phase = Phase::Cause; Ok({ - let __pending_field_receiver = receiver; + let __pending_field_receiver = runtime.into_jsvalue(receiver)?; let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Cause)?; let __pending_field_resume = self; @@ -380,13 +399,9 @@ impl ErrorResume { self.0.phase = Phase::Aggregate; Ok({ let __pending_field_iterable = - self.0 - .arguments - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "AggregateError errors argv missing", - ))?; + runtime.into_jsvalue(self.0.arguments.first().cloned().ok_or( + RuntimeError::Invariant("AggregateError errors argv missing"), + )?)?; let __pending_field_resume = self; ErrorStep::request_aggregate(__pending_field_iterable, __pending_field_resume) }) @@ -397,7 +412,9 @@ impl ErrorResume { fn finish(self, runtime: &Runtime) -> Result { let value = Value::Object(self.object()?); runtime.ensure_error_backtrace(&value, true, None)?; - Ok(ErrorStep::Complete(Completion::Return(value))) + Ok(ErrorStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))) } } pub(crate) fn finish( @@ -409,7 +426,7 @@ pub(crate) fn finish( step = match step { ErrorStep::Complete(result) => return Ok(result), ErrorStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -417,7 +434,7 @@ pub(crate) fn finish( )? } ErrorStep::String { mut resume } => { - let value = resume.take_string_value(); + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } ErrorStep::Has { mut resume } => { @@ -429,7 +446,8 @@ pub(crate) fn finish( )? } ErrorStep::Aggregate { mut resume } => { - let iterable = resume.take_aggregate_iterable(); + let iterable = + runtime.root_and_release_jsvalue(resume.take_aggregate_iterable())?; resume.resume( runtime, super::aggregate::finish( @@ -445,20 +463,24 @@ pub(crate) fn finish( #[derive(Default)] struct ErrorStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, - string_value: Option, + string_value: Option, has_object: Option, has_key: Option, - aggregate_iterable: Option, + aggregate_iterable: Option, } impl ErrorStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: ErrorResume) -> Self { + pub(crate) fn request_read( + receiver: JsValue, + key: PropertyKey, + mut resume: ErrorResume, + ) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_string(value: Value, mut resume: ErrorResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: ErrorResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } @@ -471,13 +493,13 @@ impl ErrorStep { resume.0.pending_effect.has_key = Some(key); Self::Has { resume } } - pub(crate) fn request_aggregate(iterable: Value, mut resume: ErrorResume) -> Self { + pub(crate) fn request_aggregate(iterable: JsValue, mut resume: ErrorResume) -> Self { resume.0.pending_effect.aggregate_iterable = Some(iterable); Self::Aggregate { resume } } } impl ErrorResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -491,7 +513,7 @@ impl ErrorResume { .take() .expect("ErrorStep Read key") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value @@ -512,7 +534,7 @@ impl ErrorResume { .take() .expect("ErrorStep Has key") } - pub(crate) fn take_aggregate_iterable(&mut self) -> Value { + pub(crate) fn take_aggregate_iterable(&mut self) -> JsValue { self.0 .pending_effect .aggregate_iterable diff --git a/src/engine/builtins/eval.rs b/src/engine/builtins/eval.rs index c32b0989..49f77a94 100644 --- a/src/engine/builtins/eval.rs +++ b/src/engine/builtins/eval.rs @@ -66,18 +66,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "global eval used an unexpected native invocation protocol", )); }; - let input = arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined); + invocation.release(self)?; + let input = match arguments.readable.first() { + Some(value) => self.root_and_release_jsvalue(self.dup_jsvalue(value)?)?, + None => Value::Undefined, + }; let Value::String(source) = input else { - return Ok(Completion::Return(input)); + return Ok(Completion::Return(self.into_jsvalue(input)?)); }; self.execute_indirect_string_eval(realm, &source) } @@ -100,7 +101,6 @@ impl Runtime { input, environment: environment_index, this_value, - new_target: _, caller_strict, } = invocation; if !matches!(input, Value::String(_)) { @@ -109,7 +109,9 @@ impl Runtime { "non-String direct eval prepared a caller environment", )); } - return Ok(DirectEvalPreparation::Complete(Completion::Return(input))); + return Ok(DirectEvalPreparation::Complete(Completion::Return( + self.into_jsvalue(input)?, + ))); } let environment = environment.ok_or(RuntimeError::Invariant( @@ -156,7 +158,9 @@ impl Runtime { )? { Compilation::Published(function) => function, Compilation::Throw(value) => { - return Ok(DirectEvalPreparation::Complete(Completion::Throw(value))); + return Ok(DirectEvalPreparation::Complete(Completion::Throw( + self.into_jsvalue(value)?, + ))); } }; @@ -191,6 +195,19 @@ impl Runtime { }) } + /// Internal-value form of [`Runtime::is_original_eval`]. + pub(crate) fn is_original_eval_jsvalue( + &self, + realm: ContextId, + function: &crate::engine::value::JsValue, + ) -> Result { + let crate::engine::value::JsValue::Object(object) = function else { + return Ok(false); + }; + let object = crate::engine::object::ObjectRef::from_borrowed_handle(self.clone(), *object)?; + self.is_original_eval(realm, &Value::Object(object)) + } + pub(crate) fn is_original_eval( &self, realm: ContextId, @@ -418,7 +435,9 @@ impl Runtime { match self.compile_eval_in_realm(realm, &source, DEFAULT_EVAL_FILENAME, context)? { Compilation::Published(function) => function, Compilation::Throw(value) => { - return Ok(DirectEvalPreparation::Complete(Completion::Throw(value))); + return Ok(DirectEvalPreparation::Complete(Completion::Throw( + self.into_jsvalue(value)?, + ))); } }; let callable = diff --git a/src/engine/builtins/function.rs b/src/engine/builtins/function.rs index 1c1e48d9..f3179e20 100644 --- a/src/engine/builtins/function.rs +++ b/src/engine/builtins/function.rs @@ -7,7 +7,7 @@ use crate::engine::code::function::metadata::FunctionKind; use crate::engine::heap::{ContextId, ObjectPayload}; use crate::engine::object::CallableRef; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -40,7 +40,9 @@ impl Runtime { // explicit part of QuickJS's `b->has_prototype` test. let sloppy_legacy_get = if arguments.actual_arg_count == 0 { match this_value { - Value::Object(object) => { + JsValue::Object(id) => { + let object = + crate::engine::object::ObjectRef::from_borrowed_handle(self.clone(), *id)?; let state = self.0.state.borrow(); let object = state.heap.object(object.object_id())?; match object.payload { @@ -88,22 +90,22 @@ impl Runtime { | ObjectPayload::AsyncGenerator(_) => false, } } - Value::Undefined - | Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_) - | Value::String(_) - | Value::BigInt(_) - | Value::Symbol(_) => false, + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) + | JsValue::String(_) + | JsValue::BigInt(_) + | JsValue::Symbol(_) => false, } } else { false }; if sloppy_legacy_get { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); } - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "invalid property access", @@ -117,11 +119,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - dynamic::finish( - self, - realm, - dynamic::DynamicFunctionStep::start(self, realm, kind, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + dynamic::finish( + self, + realm, + dynamic::DynamicFunctionStep::start(self, realm, kind, invocation, arguments)?, + ) + }) } pub(crate) fn call_function_prototype_apply( @@ -130,17 +134,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - super::function::invoke::finish( - self, - realm, - super::function::invoke::InvokeStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + super::function::invoke::finish( self, realm, - super::function::invoke::InvokeKind::Apply, - &invocation, - arguments, - )?, - ) + super::function::invoke::InvokeStep::start( + self, + realm, + super::function::invoke::InvokeKind::Apply, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_function_prototype_bind( @@ -149,11 +155,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - bind::finish( - self, - realm, - bind::BindStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + bind::finish( + self, + realm, + bind::BindStep::start(self, realm, invocation, arguments)?, + ) + }) } pub(crate) fn call_function_prototype_to_string( @@ -161,11 +169,13 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - text::finish( - self, - realm, - text::FunctionTextStep::start(self, realm, &invocation)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( + self, + realm, + text::FunctionTextStep::start(self, realm, invocation)?, + ) + }) } pub(crate) fn call_function_prototype_file_name( @@ -177,14 +187,15 @@ impl Runtime { "Function.prototype.fileName getter received the wrong native invocation", )); }; - let Value::Object(function) = this_value else { - return Ok(Completion::Return(Value::Undefined)); + let JsValue::Object(id) = this_value else { + return Ok(Completion::Return(JsValue::Undefined)); }; + let function = crate::engine::object::ObjectRef::from_borrowed_handle(self.clone(), *id)?; let filename = { let state = self.0.state.borrow(); let object = state.heap.object(function.object_id())?; let ObjectPayload::BytecodeFunction { bytecode, .. } = &object.payload else { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); }; let bytecode = state.heap.function_bytecode(*bytecode)?; bytecode @@ -193,9 +204,10 @@ impl Runtime { .map(|debug| state.atoms.to_js_string(debug.filename)) .transpose()? }; - Ok(Completion::Return( - filename.map_or(Value::Undefined, Value::String), - )) + Ok(Completion::Return(match filename { + Some(filename) => self.unroot_value(&Value::String(filename))?, + None => JsValue::Undefined, + })) } pub(crate) fn call_function_prototype_position( @@ -208,14 +220,15 @@ impl Runtime { "Function.prototype position getter received the wrong native invocation", )); }; - let Value::Object(function) = this_value else { - return Ok(Completion::Return(Value::Undefined)); + let JsValue::Object(id) = this_value else { + return Ok(Completion::Return(JsValue::Undefined)); }; + let function = crate::engine::object::ObjectRef::from_borrowed_handle(self.clone(), *id)?; let position = { let state = self.0.state.borrow(); let object = state.heap.object(function.object_id())?; let ObjectPayload::BytecodeFunction { bytecode, .. } = &object.payload else { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); }; let bytecode = state.heap.function_bytecode(*bytecode)?; bytecode @@ -224,10 +237,10 @@ impl Runtime { .map(|debug| debug.pc2line.as_ref().map(|table| table.lookup(None))) }; let Some(position) = position else { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); }; let Some(position) = position else { - return Ok(Completion::Return(Value::Int(0))); + return Ok(Completion::Return(JsValue::Int(0))); }; let (line, column) = position.one_based().ok_or(RuntimeError::Invariant( "function definition position cannot be represented one-based", @@ -239,7 +252,7 @@ impl Runtime { let selected = i32::try_from(selected).map_err(|_| { RuntimeError::Invariant("function definition position does not fit Int32") })?; - Ok(Completion::Return(Value::Int(selected))) + Ok(Completion::Return(JsValue::Int(selected))) } pub(crate) fn call_function_prototype_has_instance( @@ -248,11 +261,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - instance::finish( - self, - realm, - instance::InstanceStep::native(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + instance::finish( + self, + realm, + instance::InstanceStep::native(self, realm, invocation, arguments)?, + ) + }) } pub(crate) fn ordinary_is_instance_of( &self, diff --git a/src/engine/builtins/function/arguments.rs b/src/engine/builtins/function/arguments.rs index edc7c011..9f4510f7 100644 --- a/src/engine/builtins/function/arguments.rs +++ b/src/engine/builtins/function/arguments.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, HeapError}, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::Completion, }; @@ -17,7 +17,7 @@ pub(crate) enum ArgumentsStep { resume: ArgumentsResume, }, Number { - value: Value, + value: JsValue, resume: ArgumentsResume, }, } @@ -76,16 +76,18 @@ impl ArgumentsResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { - return Ok(ArgumentsStep::Complete(NativeConversion::Throw(value))); + return Ok(ArgumentsStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } }; match std::mem::replace(&mut self.0.phase, Phase::Number) { Phase::Length => { self.0.phase = Phase::Number; Ok(ArgumentsStep::Number { - value, + value: runtime.into_jsvalue(value)?, resume: self, }) } @@ -181,6 +183,7 @@ pub(crate) fn finish( runtime.get_property_in_realm(realm, &object, &key)?, )?, ArgumentsStep::Number { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } }; diff --git a/src/engine/builtins/function/bind.rs b/src/engine/builtins/function/bind.rs index 59b00d19..f0ed5fc5 100644 --- a/src/engine/builtins/function/bind.rs +++ b/src/engine/builtins/function/bind.rs @@ -4,7 +4,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -55,22 +55,29 @@ impl BindStep { )); }; let target = match this_value { - Value::Object(object) => runtime.as_callable(object)?, + JsValue::Object(id) => { + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; + runtime.as_callable(&object)? + } _ => None, }; let Some(target) = target else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not a function")?, ))); }; let count = arguments.actual_arg_count.saturating_sub(1); - let forwarded = if arguments.actual_arg_count > 1 { - &arguments.readable[1..arguments.actual_arg_count] - } else { - &[] - }; - let bound = - runtime.new_bound_function(realm, &target, &arguments.readable[0], forwarded)?; + let mut forwarded = Vec::new(); + if arguments.actual_arg_count > 1 { + forwarded + .try_reserve_exact(count) + .map_err(|_| RuntimeError::Invariant("bind argv allocation failed"))?; + for value in &arguments.readable[1..arguments.actual_arg_count] { + forwarded.push(runtime.root_value(value)?); + } + } + let this_argument = runtime.root_value(&arguments.readable[0])?; + let bound = runtime.new_bound_function(realm, &target, &this_argument, &forwarded)?; Ok(Self::Own { object: target.as_object().clone(), key: runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, @@ -90,7 +97,9 @@ impl BindResume { result: NativeConversion, ) -> Result { match result { - NativeConversion::Throw(value) => Ok(BindStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(BindStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), NativeConversion::Value(true) => Ok(BindStep::Read { object: self.0.target.clone(), key: runtime @@ -121,7 +130,7 @@ impl BindResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, result @ Completion::Throw(_) => return Ok(BindStep::Complete(result)), }; if !self.0.name { @@ -140,8 +149,8 @@ impl BindResume { false, true, )?; - Ok(BindStep::Complete(Completion::Return(Value::Object( - self.0.bound.into_object(), + Ok(BindStep::Complete(Completion::Return(JsValue::Object( + self.0.bound.into_object().into_handle(), )))) } } diff --git a/src/engine/builtins/function/dynamic.rs b/src/engine/builtins/function/dynamic.rs index 08efd571..ef9b56c9 100644 --- a/src/engine/builtins/function/dynamic.rs +++ b/src/engine/builtins/function/dynamic.rs @@ -5,7 +5,7 @@ use crate::engine::{ code::dynamic_source::DynamicSourceBuilder, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -37,19 +37,38 @@ impl std::ops::DerefMut for DynamicFunctionResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct DynamicFunctionResumeState { + runtime: Runtime, pending_effect: DynamicFunctionStepPending, realm: ContextId, kind: DynamicFunctionKind, - new_target: Value, - arguments: Vec, + new_target: JsValue, + arguments: Vec, index: usize, source: Option, phase: Phase, value: Value, } +impl Drop for DynamicFunctionResumeState { + /// Release the internal edges still owned when the request is abandoned. + /// Consumption goes through `Option::take`/`mem::replace`, so drained + /// fields are `None`/`Undefined` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.string_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + for value in self.arguments.drain(..) { + let _ = self.runtime.release_jsvalue(value); + } + let new_target = std::mem::replace(&mut self.new_target, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(new_target); + } +} impl DynamicFunctionStep { pub(crate) fn start( - _runtime: &Runtime, + runtime: &Runtime, realm: ContextId, kind: DynamicFunctionKind, invocation: &NativeInvocation, @@ -76,18 +95,26 @@ impl DynamicFunctionStep { source.push_str("*")?; } source.push_str(" anonymous(")?; + let mut owned_arguments = Vec::new(); + owned_arguments + .try_reserve_exact(arguments.actual_arg_count) + .map_err(|_| RuntimeError::Invariant("Function constructor argv allocation failed"))?; + for value in &arguments.readable[..arguments.actual_arg_count] { + owned_arguments.push(runtime.dup_jsvalue(value)?); + } DynamicFunctionResume(Box::new(DynamicFunctionResumeState { + runtime: runtime.clone(), pending_effect: DynamicFunctionStepPending::default(), realm, kind, - new_target: new_target.clone(), - arguments: arguments.readable[..arguments.actual_arg_count].to_vec(), + new_target: runtime.dup_jsvalue(new_target)?, + arguments: owned_arguments, index: 0, source: Some(source), phase: Phase::Parameters, value: Value::Undefined, })) - .parameter() + .parameter(runtime) } } impl DynamicFunctionResume { @@ -97,19 +124,20 @@ impl DynamicFunctionResume { .as_mut() .ok_or(RuntimeError::Invariant("Function source builder missing")) } - fn parameter(mut self) -> Result { + fn parameter(mut self, runtime: &Runtime) -> Result { if self.0.index < self.0.arguments.len().saturating_sub(1) { if self.0.index != 0 { self.source()?.push_str(",")?; } return Ok({ - let __pending_field_value = self.0.arguments[self.0.index].clone(); + let __pending_field_value = runtime.dup_jsvalue(&self.0.arguments[self.0.index])?; let __pending_field_resume = self; DynamicFunctionStep::request_string(__pending_field_value, __pending_field_resume) }); } self.source()?.push_str("\n) {\n")?; - if let Some(value) = self.0.arguments.last().cloned() { + if let Some(value) = self.0.arguments.last() { + let value = runtime.dup_jsvalue(value)?; self.0.phase = Phase::Body; return Ok({ let __pending_field_value = value; @@ -121,19 +149,22 @@ impl DynamicFunctionResume { } pub(crate) fn string( mut self, + runtime: &Runtime, result: NativeConversion, ) -> Result { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(DynamicFunctionStep::Complete(Completion::Throw(value))); + return Ok(DynamicFunctionStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.source()?.push_js_string(&value)?; match self.0.phase { Phase::Parameters => { self.0.index += 1; - self.parameter() + self.parameter(runtime) } Phase::Body => self.eval(), _ => Err(RuntimeError::Invariant( @@ -162,42 +193,42 @@ impl DynamicFunctionResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(DynamicFunctionStep::Complete(Completion::Throw(value))); } }; match self.0.phase { Phase::Eval => { - if matches!(self.0.new_target, Value::Undefined) { - return Ok(DynamicFunctionStep::Complete(Completion::Return(value))); + if matches!(self.0.new_target, JsValue::Undefined) { + return Ok(DynamicFunctionStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))); } self.0.value = value; self.0.phase = Phase::Prototype; - Ok({ - let __pending_field_receiver = self.0.new_target.clone(); - let __pending_field_key = runtime - .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?; - let __pending_field_resume = self; - DynamicFunctionStep::request_read( - __pending_field_receiver, - __pending_field_key, - __pending_field_resume, - ) - }) + Ok(DynamicFunctionStep::request_read( + runtime.dup_jsvalue(&self.0.new_target)?, + runtime + .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?, + self, + )) } Phase::Prototype => { let prototype = if let Value::Object(object) = value { object } else { - let realm = match runtime - .function_realm_from_value(self.0.realm, &self.0.new_target)? - { - NativeConversion::Value(realm) => realm, - NativeConversion::Throw(value) => { - return Ok(DynamicFunctionStep::Complete(Completion::Throw(value))); - } - }; + let new_target = std::mem::replace(&mut self.0.new_target, JsValue::Undefined); + let new_target = runtime.root_and_release_jsvalue(new_target)?; + let realm = + match runtime.function_realm_from_value(self.0.realm, &new_target)? { + NativeConversion::Value(realm) => realm, + NativeConversion::Throw(value) => { + return Ok(DynamicFunctionStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; let prototype = { let state = runtime.0.state.borrow(); let context = state.heap.context(realm)?; @@ -210,9 +241,11 @@ impl DynamicFunctionResume { }; ObjectRef::from_borrowed_handle(runtime.clone(), prototype)? }; - let Value::Object(function) = self.0.value else { + let Value::Object(function) = + std::mem::replace(&mut self.0.value, Value::Undefined) + else { return Ok(DynamicFunctionStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", @@ -221,7 +254,7 @@ impl DynamicFunctionResume { }; if !runtime.set_prototype_of(&function, Some(&prototype))? { return Ok(DynamicFunctionStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "prototype is immutable", @@ -229,7 +262,7 @@ impl DynamicFunctionResume { ))); } Ok(DynamicFunctionStep::Complete(Completion::Return( - Value::Object(function), + JsValue::Object(function.into_handle()), ))) } _ => Err(RuntimeError::Invariant( @@ -247,8 +280,11 @@ pub(crate) fn finish( step = match step { DynamicFunctionStep::Complete(result) => return Ok(result), DynamicFunctionStep::String { mut resume } => { - let value = resume.take_string_value(); - resume.string(runtime.native_to_dynamic_source_fragment(realm, &value)?)? + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; + resume.string( + runtime, + runtime.native_to_dynamic_source_fragment(realm, &value)?, + )? } DynamicFunctionStep::Eval { mut resume } => { let source = resume.take_eval_source(); @@ -258,7 +294,7 @@ pub(crate) fn finish( )? } DynamicFunctionStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -271,13 +307,13 @@ pub(crate) fn finish( #[derive(Default)] struct DynamicFunctionStepPending { - string_value: Option, + string_value: Option, eval_source: Option, - read_receiver: Option, + read_receiver: Option, read_key: Option, } impl DynamicFunctionStep { - pub(crate) fn request_string(value: Value, mut resume: DynamicFunctionResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: DynamicFunctionResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } @@ -286,7 +322,7 @@ impl DynamicFunctionStep { Self::Eval { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: DynamicFunctionResume, ) -> Self { @@ -296,7 +332,7 @@ impl DynamicFunctionStep { } } impl DynamicFunctionResume { - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value @@ -310,7 +346,7 @@ impl DynamicFunctionResume { .take() .expect("DynamicFunctionStep Eval source") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver diff --git a/src/engine/builtins/function/instance.rs b/src/engine/builtins/function/instance.rs index 8ee5bbb9..9643ac98 100644 --- a/src/engine/builtins/function/instance.rs +++ b/src/engine/builtins/function/instance.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::NativeFunctionId, heap::{ContextId, ObjectPayload}, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -90,21 +90,23 @@ impl InstanceStep { )); }; let target = match this_value { - Value::Object(target) => runtime.as_callable(target)?, + JsValue::Object(id) => { + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; + runtime.as_callable(&object)? + } _ => None, }; let Some(target) = target else { - return Ok(Self::Complete(Completion::Return(Value::Bool(false)))); + return Ok(Self::Complete(Completion::Return(JsValue::Bool(false)))); }; Self::ordinary( runtime, realm, &target, - arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined), + match arguments.readable.first() { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + }, ) } pub(crate) fn ordinary( @@ -132,7 +134,7 @@ impl InstanceStep { return Self::method(runtime, realm, candidate, target, true); } if !matches!(candidate, Value::Object(_)) { - return Ok(Self::Complete(Completion::Return(Value::Bool(false)))); + return Ok(Self::Complete(Completion::Return(JsValue::Bool(false)))); } Ok({ let __pending_field_object = target.as_object().clone(); @@ -160,7 +162,7 @@ impl InstanceResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, result @ Completion::Throw(_) => return Ok(InstanceStep::Complete(result)), }; match self.0.phase { @@ -169,7 +171,7 @@ impl InstanceResume { let Some(target) = runtime.as_callable(&self.0.target)? else { return if delegate { Ok(InstanceStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "invalid 'instanceof' right operand", @@ -195,7 +197,7 @@ impl InstanceResume { if delegate && error.kind() == ErrorKind::Type => { return Ok(InstanceStep::Complete(Completion::Throw( - runtime.new_native_error_from_error( + runtime.new_native_error_from_error_jsvalue( self.0.realm, NativeErrorKind::Type, &error, @@ -206,8 +208,11 @@ impl InstanceResume { }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Object(self.0.target.clone()); - let __pending_field_arguments = vec![self.0.candidate.clone()]; + let __pending_field_receiver = + runtime.into_jsvalue(Value::Object(self.0.target.clone()))?; + let __pending_field_arguments = vec![runtime.into_jsvalue( + std::mem::replace(&mut self.0.candidate, Value::Undefined), + )?]; let __pending_field_delegate = delegate; let __pending_field_resume = { let updated_0 = Phase::Result; @@ -223,13 +228,13 @@ impl InstanceResume { ) }) } - Phase::Result => Ok(InstanceStep::Complete(Completion::Return(Value::Bool( + Phase::Result => Ok(InstanceStep::Complete(Completion::Return(JsValue::Bool( runtime.value_to_boolean(&value)?, )))), Phase::Prototype => { let Value::Object(prototype) = value else { return Ok(InstanceStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "operand 'prototype' property is not an object", @@ -256,6 +261,7 @@ impl InstanceResume { } pub(crate) fn prototype( self, + runtime: &Runtime, result: NativeConversion>, ) -> Result { let Phase::Walk(expected) = &self.0.phase else { @@ -264,12 +270,14 @@ impl InstanceResume { )); }; Ok(match result { - NativeConversion::Throw(value) => InstanceStep::Complete(Completion::Throw(value)), + NativeConversion::Throw(value) => { + InstanceStep::Complete(Completion::Throw(runtime.into_jsvalue(value)?)) + } NativeConversion::Value(None) => { - InstanceStep::Complete(Completion::Return(Value::Bool(false))) + InstanceStep::Complete(Completion::Return(JsValue::Bool(false))) } NativeConversion::Value(Some(object)) if &object == expected => { - InstanceStep::Complete(Completion::Return(Value::Bool(true))) + InstanceStep::Complete(Completion::Return(JsValue::Bool(true))) } NativeConversion::Value(Some(object)) => { let __pending_field_object = object; @@ -299,7 +307,7 @@ pub(super) fn finish( } InstanceStep::Prototype { mut resume } => { let object = resume.take_prototype_object(); - resume.prototype(runtime.internal_get_prototype_of(realm, &object)?)? + resume.prototype(runtime, runtime.internal_get_prototype_of(realm, &object)?)? } InstanceStep::Call { mut resume } => { let callable = resume.take_call_callable(); @@ -326,18 +334,26 @@ pub(super) fn finish( 1usize.max(usize::from(min)), )?); realm = defining_realm; - InstanceStep::native( - runtime, - realm, - &NativeInvocation::Call { - this_value: receiver, - }, - &NativeArguments { - actual_arg_count: 1, - readable: arguments, - }, - )? + let invocation = NativeInvocation::Call { + this_value: receiver, + }; + runtime.dispatch_borrowed_invocation(invocation, |invocation| { + InstanceStep::native( + runtime, + realm, + invocation, + &NativeArguments { + actual_arg_count: 1, + readable: arguments, + }, + ) + })? } else { + let receiver = runtime.root_and_release_jsvalue(receiver)?; + let arguments = arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -361,8 +377,8 @@ struct InstanceStepPending { read_object: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, call_delegate: Option, prototype_object: Option, } @@ -378,8 +394,8 @@ impl InstanceStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, delegate: bool, mut resume: InstanceResume, ) -> Self { @@ -416,14 +432,14 @@ impl InstanceResume { .take() .expect("InstanceStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("InstanceStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/function/invoke.rs b/src/engine/builtins/function/invoke.rs index 0715f549..59b50819 100644 --- a/src/engine/builtins/function/invoke.rs +++ b/src/engine/builtins/function/invoke.rs @@ -6,7 +6,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, builtins::native::NativeFunctionId, heap::ContextId, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -80,34 +80,32 @@ impl InvokeStep { }; if matches!(kind, InvokeKind::ReflectConstruct) { let new_target = if arguments.actual_arg_count > 2 { - let value = arguments - .readable - .get(2) - .cloned() - .ok_or(RuntimeError::Invariant( - "Reflect.construct newTarget argv was not readable", - ))?; + let value = runtime.root_value(arguments.readable.get(2).ok_or( + RuntimeError::Invariant("Reflect.construct newTarget argv was not readable"), + )?)?; if !matches!(value, Value::Object(_)) { - return Ok(Self::Complete(Completion::Throw( + return Ok(Self::Complete(Completion::Throw(runtime.into_jsvalue( runtime.new_not_constructor_error(realm, &value)?, - ))); + )?))); } Some(match runtime.constructor_from_value(realm, value)? { NativeConversion::Value(target) => ConstructNewTarget::Validated(target), NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }) } else { None }; return Ok({ - let __pending_field_value = arguments.readable[1].clone(); + let __pending_field_value = runtime.dup_jsvalue(&arguments.readable[1])?; let __pending_field_resume = InvokeResume(Box::new(InvokeResumeState { pending_effect: InvokeStepPending::default(), realm, target: ForwardTarget::Construct { - target: arguments.readable[0].clone(), + target: runtime.root_value(&arguments.readable[0])?, new_target, }, })); @@ -117,65 +115,87 @@ impl InvokeStep { let (target, receiver, list) = match kind { InvokeKind::ReflectConstruct => unreachable!("constructor validation already handled"), InvokeKind::Call => { - let actual = &arguments.readable[..arguments.actual_arg_count]; + let mut actual = Vec::new(); + actual + .try_reserve_exact(arguments.actual_arg_count) + .map_err(|_| RuntimeError::Invariant("function.call argv allocation failed"))?; + for value in &arguments.readable[..arguments.actual_arg_count] { + actual.push(runtime.root_value(value)?); + } let (target, receiver) = match runtime.forward_function_prototype_call( realm, - this_value.clone(), - actual, + runtime.root_value(this_value)?, + &actual, )? { NativeConversion::Value(result) => result, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; - let forwarded = actual.get(1..).unwrap_or(&[]).to_vec(); + let forwarded = actual + .into_iter() + .skip(1) + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; #[cfg(feature = "profiling")] { crate::engine::api::profiling::record_call_buffer_capacity( "function.call_suffix", 0, forwarded.capacity(), - size_of::(), + size_of::(), ); - crate::engine::api::profiling::record_call_buffer_copies( + crate::engine::api::profiling::record_call_buffer_js_value_copies( "function.call_suffix", &forwarded, ); } return Ok(Self::Call(Box::new(InvokeCall { target, - receiver, + receiver: runtime.into_jsvalue(receiver)?, arguments: forwarded, }))); } InvokeKind::Apply => { let target = match this_value { - Value::Object(object) => runtime.as_callable(object)?, + JsValue::Object(id) => { + let object = crate::engine::object::ObjectRef::from_borrowed_handle( + runtime.clone(), + *id, + )?; + runtime.as_callable(&object)? + } _ => None, }; let Some(target) = target else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; ( DirectCallTarget::Callable(target), - arguments.readable[0].clone(), - arguments.readable[1].clone(), + runtime.root_value(&arguments.readable[0])?, + runtime.dup_jsvalue(&arguments.readable[1])?, ) } InvokeKind::ReflectApply => ( DirectCallTarget::Callable( - runtime.callable_from_value(arguments.readable[0].clone())?, + runtime.callable_from_value(runtime.root_value(&arguments.readable[0])?)?, ), - arguments.readable[1].clone(), - arguments.readable[2].clone(), + runtime.root_value(&arguments.readable[1])?, + runtime.dup_jsvalue(&arguments.readable[2])?, ), }; - if matches!(kind, InvokeKind::Apply) && matches!(list, Value::Null | Value::Undefined) { + if matches!(kind, InvokeKind::Apply) && matches!(list, JsValue::Null | JsValue::Undefined) { return Ok(Self::Call(Box::new(InvokeCall { target, - receiver, + receiver: runtime.into_jsvalue(receiver)?, arguments: Vec::new(), }))); } @@ -203,7 +223,7 @@ impl InvokeStep { if matches!(value, Value::Null | Value::Undefined) { return Ok(Self::Call(Box::new(InvokeCall { target: DirectCallTarget::Callable(callable), - receiver, + receiver: runtime.into_jsvalue(receiver)?, arguments: Vec::new(), }))); } @@ -214,11 +234,11 @@ impl InvokeStep { }, crate::engine::code::bytecode::ApplyKind::Construct => ForwardTarget::Construct { target, - new_target: Some(ConstructNewTarget::Raw(receiver)), + new_target: Some(ConstructNewTarget::Raw(runtime.into_jsvalue(receiver)?)), }, }; Ok({ - let __pending_field_value = value; + let __pending_field_value = runtime.into_jsvalue(value)?; let __pending_field_resume = InvokeResume(Box::new(InvokeResumeState { pending_effect: InvokeStepPending::default(), realm, @@ -236,27 +256,34 @@ impl InvokeResume { ) -> Result { let arguments = match result { NativeConversion::Throw(value) => { - return Ok(InvokeStep::Complete(Completion::Throw(value))); + return Ok(InvokeStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } - NativeConversion::Value(arguments) => arguments, + NativeConversion::Value(arguments) => arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, }; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_call_buffer_observed( "invoke.argv_carrier", arguments.capacity(), - size_of::(), + size_of::(), ); Ok(match self.0.target { ForwardTarget::Call { target, receiver } => InvokeStep::Call(Box::new(InvokeCall { target, - receiver, + receiver: runtime.into_jsvalue(receiver)?, arguments, })), ForwardTarget::Construct { target, new_target } => { let target = match runtime.constructor_from_value(self.0.realm, target)? { NativeConversion::Value(target) => target, NativeConversion::Throw(value) => { - return Ok(InvokeStep::Complete(Completion::Throw(value))); + return Ok(InvokeStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let new_target = @@ -281,7 +308,11 @@ pub(crate) fn finish( InvokeStep::Construct(request) => { let target = request.target; let new_target = request.new_target; - let arguments = request.arguments; + let arguments = request + .arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { return runtime.construct_internal_with_new_target( realm, &target, new_target, &arguments, @@ -292,13 +323,25 @@ pub(crate) fn finish( let value = resume.take_arguments_value(); resume.arguments( runtime, - finish_arguments(runtime, realm, ArgumentsStep::start(runtime, realm, value)?)?, + finish_arguments( + runtime, + realm, + ArgumentsStep::start( + runtime, + realm, + runtime.root_and_release_jsvalue(value)?, + )?, + )?, )? } InvokeStep::Call(request) => { let target = request.target; - let receiver = request.receiver; - let arguments = request.arguments; + let receiver = runtime.root_and_release_jsvalue(request.receiver)?; + let arguments = request + .arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { return match target { DirectCallTarget::Callable(target) => { @@ -316,16 +359,16 @@ pub(crate) fn finish( #[derive(Default)] struct InvokeStepPending { - arguments_value: Option, + arguments_value: Option, } impl InvokeStep { - pub(crate) fn request_arguments(value: Value, mut resume: InvokeResume) -> Self { + pub(crate) fn request_arguments(value: JsValue, mut resume: InvokeResume) -> Self { resume.0.pending_effect.arguments_value = Some(value); Self::Arguments { resume } } } impl InvokeResume { - pub(crate) fn take_arguments_value(&mut self) -> Value { + pub(crate) fn take_arguments_value(&mut self) -> JsValue { self.0 .pending_effect .arguments_value @@ -337,14 +380,14 @@ const _: () = assert!(std::mem::size_of::() <= 64); pub(crate) struct InvokeCall { pub(crate) target: DirectCallTarget, - pub(crate) receiver: Value, - pub(crate) arguments: Vec, + pub(crate) receiver: JsValue, + pub(crate) arguments: Vec, } pub(crate) struct InvokeConstruct { pub(crate) target: ConstructorRef, pub(crate) new_target: ConstructNewTarget, - pub(crate) arguments: Vec, + pub(crate) arguments: Vec, } // S11 all-domain protocol bound; inline completion stays allocation-free. diff --git a/src/engine/builtins/function/text.rs b/src/engine/builtins/function/text.rs index b8d552e5..1fee4017 100644 --- a/src/engine/builtins/function/text.rs +++ b/src/engine/builtins/function/text.rs @@ -4,7 +4,7 @@ use crate::engine::{ code::function::metadata::FunctionKind, heap::{ContextId, ObjectPayload}, object::{ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeInvocation}, }; pub(crate) enum FunctionTextStep { @@ -37,16 +37,17 @@ impl FunctionTextStep { realm: ContextId, invocation: &NativeInvocation, ) -> Result { - let NativeInvocation::Call { this_value } = invocation.clone() else { + let NativeInvocation::Call { this_value } = invocation else { return Err(RuntimeError::Invariant( "Function.prototype.toString did not receive a generic invocation", )); }; - let Value::Object(function) = this_value else { + let JsValue::Object(id) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not a function")?, ))); }; + let function = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; let (is_callable, source, function_kind) = { let state = runtime.0.state.borrow(); @@ -104,13 +105,13 @@ impl FunctionTextStep { }; if !is_callable { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not a function")?, ))); } if let Some(source) = source { - return Ok(Self::Complete(Completion::Return(Value::String( - JsString::try_from_bytes(&source)?, - )))); + return Ok(Self::Complete(Completion::Return(runtime.unroot_value( + &Value::String(JsString::try_from_bytes(&source)?), + )?))); } Ok({ @@ -132,7 +133,11 @@ impl FunctionTextStep { } } impl FunctionTextResume { - pub(crate) fn resume(mut self, result: Completion) -> Result { + pub(crate) fn resume( + mut self, + runtime: &Runtime, + result: Completion, + ) -> Result { if self.converted { return Err(RuntimeError::Invariant("Function name repeated reply")); } @@ -143,8 +148,8 @@ impl FunctionTextResume { } }; self.converted = true; - if matches!(value, Value::Undefined) { - self.string(NativeConversion::Value(JsString::from_static(""))) + if matches!(value, JsValue::Undefined) { + self.string(runtime, NativeConversion::Value(JsString::from_static(""))) } else { Ok({ let __pending_field_value = value; @@ -155,6 +160,7 @@ impl FunctionTextResume { } pub(crate) fn string( self, + runtime: &Runtime, result: NativeConversion, ) -> Result { if !self.converted { @@ -163,7 +169,9 @@ impl FunctionTextResume { let name = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(FunctionTextStep::Complete(Completion::Throw(value))); + return Ok(FunctionTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let prefix = match self.kind { @@ -177,7 +185,7 @@ impl FunctionTextResume { .try_concat(&JsString::from_static("() {\n [native code]\n}"))?; drop(self.0.function); Ok(FunctionTextStep::Complete(Completion::Return( - Value::String(value), + runtime.unroot_value(&Value::String(value))?, ))) } } @@ -192,11 +200,14 @@ pub(crate) fn finish( FunctionTextStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - resume.resume(runtime.get_property_in_realm(realm, &object, &key)?)? + resume.resume( + runtime, + runtime.get_property_in_realm(realm, &object, &key)?, + )? } FunctionTextStep::String { mut resume } => { - let value = resume.take_string_value(); - resume.string(runtime.native_to_js_string(realm, &value)?)? + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; + resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } }; } @@ -206,7 +217,7 @@ pub(crate) fn finish( struct FunctionTextStepPending { read_object: Option, read_key: Option, - string_value: Option, + string_value: Option, } impl FunctionTextStep { pub(crate) fn request_read( @@ -218,7 +229,7 @@ impl FunctionTextStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_string(value: Value, mut resume: FunctionTextResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: FunctionTextResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } @@ -238,7 +249,7 @@ impl FunctionTextResume { .take() .expect("FunctionTextStep Read key") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value diff --git a/src/engine/builtins/iterator/array.rs b/src/engine/builtins/iterator/array.rs index 384505fe..a44b2019 100644 --- a/src/engine/builtins/iterator/array.rs +++ b/src/engine/builtins/iterator/array.rs @@ -4,7 +4,7 @@ use crate::engine::{ builtins::native::ArrayIteratorKind, heap::{ContextId, HeapError}, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeInvocation, NativeInvokeOutcome}, @@ -40,7 +40,7 @@ pub(crate) struct ArrayNextResumeState { phase: Phase, requested_object: Option, requested_key: Option, - requested_value: Option, + requested_value: Option, requested_read: Option, } @@ -60,9 +60,10 @@ impl ArrayNextStep { "Array Iterator next did not receive an iterator-next invocation", )); }; - let Value::Object(iterator) = this_value else { + let JsValue::Object(iterator_id) = this_value else { return Self::wrong_receiver(runtime, realm); }; + let iterator = ObjectRef::from_borrowed_handle(runtime.clone(), *iterator_id)?; let state = runtime .0 .state @@ -76,18 +77,18 @@ impl ArrayNextStep { }; let Some(source) = source else { return Ok(Self::Complete(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, })); }; - if let Some(value) = Self::dense_immediate_next(runtime, iterator, source, index, kind)? { + if let Some(value) = Self::dense_immediate_next(runtime, &iterator, source, index, kind)? { #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( "array_next_dense_immediate_leaf", ); return Ok(Self::Complete(NativeInvokeOutcome::IteratorNextRaw { - value, + value: runtime.into_jsvalue(value)?, done: false, })); } @@ -109,7 +110,9 @@ impl ArrayNextStep { let action = match runtime.typed_array_validated_length(realm, &resume.source)? { NativeConversion::Value(length) => resume.length(runtime, length)?, NativeConversion::Throw(value) => { - NextAction::Complete(NativeInvokeOutcome::Completion(Completion::Throw(value))) + NextAction::Complete(NativeInvokeOutcome::Completion(Completion::Throw( + runtime.into_jsvalue(value)?, + ))) } }; return resume.drive(runtime, action); @@ -119,7 +122,7 @@ impl ArrayNextStep { } fn wrong_receiver(runtime: &Runtime, realm: ContextId) -> Result { Ok(Self::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "Array Iterator object expected", @@ -140,7 +143,7 @@ impl ArrayNextResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(NextAction::Complete(NativeInvokeOutcome::Completion( Completion::Throw(value), @@ -162,7 +165,7 @@ impl ArrayNextResume { value }; Ok(NextAction::Complete(NativeInvokeOutcome::IteratorNextRaw { - value, + value: runtime.into_jsvalue(value)?, done: false, })) } @@ -186,7 +189,7 @@ impl ArrayNextResume { self.length(runtime, Runtime::to_uint32_number(value)) } NativeConversion::Throw(value) => Ok(NextAction::Complete( - NativeInvokeOutcome::Completion(Completion::Throw(value)), + NativeInvokeOutcome::Completion(Completion::Throw(runtime.into_jsvalue(value)?)), )), } } @@ -198,7 +201,7 @@ impl ArrayNextResume { .finish_array_iterator(self.0.iterator.object_id())?; state.apply_cleanup(cleanup)?; return Ok(NextAction::Complete(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, })); }; @@ -210,7 +213,7 @@ impl ArrayNextResume { .set_array_iterator_index(self.0.iterator.object_id(), next_index)?; if self.0.kind == ArrayIteratorKind::Key { return Ok(NextAction::Complete(NativeInvokeOutcome::IteratorNextRaw { - value: Runtime::array_length_value(self.0.index), + value: runtime.into_jsvalue(Runtime::array_length_value(self.0.index))?, done: false, })); } @@ -256,7 +259,7 @@ impl ArrayNextResume { )? { OrdinaryRead::Complete(value) => self.resume_once( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), )?, read => { return Ok(self.prepared(read, key)); @@ -273,7 +276,7 @@ impl ArrayNextResume { }; self.number_once(runtime, reply)? } - action => return Ok(self.wait(action)), + action => return self.wait(runtime, action), }; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( @@ -308,20 +311,24 @@ impl ArrayNextResume { self.requested_key.take().expect("array next key"), ) } - pub(crate) fn take_number(&mut self) -> Value { + pub(crate) fn take_number(&mut self) -> JsValue { self.requested_value.take().expect("array next number") } - fn wait(mut self, action: NextAction) -> ArrayNextStep { + fn wait( + mut self, + runtime: &Runtime, + action: NextAction, + ) -> Result { match action { - NextAction::Complete(result) => ArrayNextStep::Complete(result), + NextAction::Complete(result) => Ok(ArrayNextStep::Complete(result)), NextAction::Read(key) => { self.requested_object = Some(self.source.clone()); self.requested_key = Some(key); - ArrayNextStep::Read { resume: self } + Ok(ArrayNextStep::Read { resume: self }) } NextAction::Number(value) => { - self.requested_value = Some(value); - ArrayNextStep::Number { resume: self } + self.requested_value = Some(runtime.into_jsvalue(value)?); + Ok(ArrayNextStep::Number { resume: self }) } } } @@ -347,9 +354,11 @@ pub(crate) fn finish( let key = resume.take_key(); let completion = match runtime.finish_prepared_read(realm, &key, read)? { NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) + Completion::Return(runtime.into_jsvalue(value.unwrap_or(Value::Undefined))?) + } + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) } - NativeConversion::Throw(value) => Completion::Throw(value), }; resume.resume(runtime, completion)? } @@ -361,7 +370,7 @@ pub(crate) fn finish( )? } ArrayNextStep::Number { mut resume } => { - let value = resume.take_number(); + let value = runtime.root_and_release_jsvalue(resume.take_number())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } }; diff --git a/src/engine/builtins/iterator/array/local.rs b/src/engine/builtins/iterator/array/local.rs index 0f635f91..3bf03985 100644 --- a/src/engine/builtins/iterator/array/local.rs +++ b/src/engine/builtins/iterator/array/local.rs @@ -53,6 +53,8 @@ impl ArrayNextStep { let length = first.atom; // Borrow the already-owned mandatory property name; a malformed or // unexpected layout falls back to the original interned-key accessor. + // The stored unbranded index is re-branded at this table boundary. + let length = state.atoms.brand(length)?; let info = state.atoms.resolve(length)?; let AtomSpelling::Text(text) = info.spelling else { return Ok(None); @@ -99,7 +101,9 @@ impl ArrayNextStep { match read { OrdinaryRead::Complete(value) => resume.resume( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return( + value.unwrap_or(crate::engine::value::JsValue::Undefined), + ), )?, read => { // Lookup may have materialized a lazy descriptor. @@ -109,9 +113,9 @@ impl ArrayNextStep { } } Self::Number { mut resume } - if !matches!(resume.requested_value, Some(Value::Object(_))) => + if !matches!(resume.requested_value, Some(JsValue::Object(_))) => { - let value = resume.take_number(); + let value = runtime.root_and_release_jsvalue(resume.take_number())?; let NumberStep::Complete(result) = NumberStep::start(runtime, realm, value)? else { return Err(RuntimeError::Invariant( @@ -161,14 +165,12 @@ mod dense_immediate_tests { let runtime = Runtime::new(); let mut context = runtime.new_context(); let iterator = context.eval("Array.prototype.values.call({get length(){return {valueOf(){return 1}}},get 0(){return 7}})").unwrap(); - let ArrayNextStep::PreparedRead { mut resume } = ArrayNextStep::start( - &runtime, - context.realm, - &NativeInvocation::Call { - this_value: iterator, - }, - ) - .unwrap() else { + let invocation = NativeInvocation::Call { + this_value: runtime.into_jsvalue(iterator).unwrap(), + }; + let step = ArrayNextStep::start(&runtime, context.realm, &invocation).unwrap(); + invocation.release(&runtime).unwrap(); + let ArrayNextStep::PreparedRead { mut resume } = step else { panic!("length getter") }; let address = &*resume.0 as *const ArrayNextResumeState; @@ -180,13 +182,19 @@ mod dense_immediate_tests { else { panic!("length") }; - let ArrayNextStep::Number { mut resume } = - resume.resume(&runtime, Completion::Return(value)).unwrap() + let ArrayNextStep::Number { mut resume } = resume + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(value).unwrap()), + ) + .unwrap() else { panic!("number") }; assert_eq!(&*resume.0 as *const ArrayNextResumeState, address); - let value = resume.take_number(); + let value = runtime + .root_and_release_jsvalue(resume.take_number()) + .unwrap(); let result = runtime.native_to_number(context.realm, &value).unwrap(); let ArrayNextStep::PreparedRead { mut resume } = resume.number(&runtime, result).unwrap() else { @@ -202,9 +210,14 @@ mod dense_immediate_tests { panic!("element") }; assert!(matches!( - resume.resume(&runtime, Completion::Return(value)).unwrap(), + resume + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(value).unwrap()) + ) + .unwrap(), ArrayNextStep::Complete(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Int(7), + value: JsValue::Int(7), done: false }) )); diff --git a/src/engine/builtins/iterator/collection.rs b/src/engine/builtins/iterator/collection.rs index b113a41a..a832bf17 100644 --- a/src/engine/builtins/iterator/collection.rs +++ b/src/engine/builtins/iterator/collection.rs @@ -13,7 +13,7 @@ use crate::engine::{ }, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{ConstructorPrototypeSource, NativeArguments, NativeInvocation}, @@ -62,6 +62,7 @@ impl std::ops::DerefMut for CollectionResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct CollectionResumeState { + runtime: Runtime, pending_effect: CollectionStepPending, realm: ContextId, kind: CollectionKind, @@ -73,6 +74,29 @@ pub(crate) struct CollectionResumeState { phase: Phase, closing: bool, } +impl Drop for CollectionResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + for value in [ + self.pending_effect.prototype_new_target.take(), + self.pending_effect.read_receiver.take(), + self.pending_effect.call_receiver.take(), + self.pending_effect.next_method.take(), + ] + .into_iter() + .flatten() + { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} enum Phase { Prototype, Adder, @@ -97,10 +121,10 @@ impl CollectionStep { "collection constructor did not receive a constructor invocation", )); }; - let _ = runtime; Ok({ - let __pending_field_new_target = new_target.clone(); + let __pending_field_new_target = runtime.dup_jsvalue(new_target)?; let __pending_field_resume = CollectionResume(Box::new(CollectionResumeState { + runtime: runtime.clone(), pending_effect: CollectionStepPending::default(), realm, kind, @@ -108,15 +132,9 @@ impl CollectionStep { iterable: if arguments.actual_arg_count == 0 { None } else { - Some( - arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "collection iterable argv was not padded", - ))?, - ) + Some(runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("collection iterable argv was not padded"), + )?)?) }, iterator: None, next: Value::Undefined, @@ -141,7 +159,7 @@ impl CollectionResume { .clone() .ok_or(RuntimeError::Invariant("collection iterator missing")) } - fn abrupt(mut self, value: Value) -> Result { + fn abrupt(mut self, runtime: &Runtime, value: Value) -> Result { if matches!( self.0.phase, Phase::Key(_) | Phase::Value { .. } | Phase::Add(_) @@ -154,7 +172,7 @@ impl CollectionResume { self.0.closing = true; return Ok({ let __pending_field_iterator = self.iterator()?; - let __pending_field_completion = Completion::Throw(value); + let __pending_field_completion = Completion::Throw(runtime.into_jsvalue(value)?); let __pending_field_resume = self; CollectionStep::request_close( __pending_field_iterator, @@ -163,13 +181,15 @@ impl CollectionResume { ) }); } - Ok(CollectionStep::Complete(Completion::Throw(value))) + Ok(CollectionStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))) } - fn next_step(mut self) -> Result { + fn next_step(mut self, runtime: &Runtime) -> Result { self.0.phase = Phase::Next; Ok({ let __pending_field_iterator = self.iterator()?; - let __pending_field_method = self.0.next.clone(); + let __pending_field_method = runtime.into_jsvalue(self.0.next.clone())?; let __pending_field_resume = self; CollectionStep::request_next( __pending_field_iterator, @@ -190,7 +210,9 @@ impl CollectionResume { } let prototype = match reply { NativeConversion::Throw(value) => { - return Ok(CollectionStep::Complete(Completion::Throw(value))); + return Ok(CollectionStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(ConstructorPrototypeSource::Explicit(prototype)) => prototype, NativeConversion::Value(ConstructorPrototypeSource::Realm(realm)) => { @@ -224,13 +246,13 @@ impl CollectionResume { .as_ref() .is_none_or(|value| matches!(value, Value::Null | Value::Undefined)) { - return Ok(CollectionStep::Complete(Completion::Return(Value::Object( - collection, - )))); + return Ok(CollectionStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(collection))?, + ))); } self.0.phase = Phase::Adder; Ok({ - let __pending_field_receiver = Value::Object(collection); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(collection))?; let __pending_field_key = runtime.intern_property_key(if self.0.kind.pairs() { "set" } else { "add" })?; let __pending_field_resume = self; @@ -250,8 +272,10 @@ impl CollectionResume { return Ok(CollectionStep::Complete(reply)); } let value = match reply { - Completion::Return(value) => value, - Completion::Throw(value) => return self.abrupt(value), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return self.abrupt(runtime, runtime.root_and_release_jsvalue(value)?); + } }; match std::mem::replace(&mut self.0.phase, Phase::Next) { Phase::Adder => { @@ -265,16 +289,17 @@ impl CollectionResume { NativeErrorKind::Type, "set/add is not a function", )?; - return self.abrupt(error); + return self.abrupt(runtime, error); }; self.0.adder = Some(callback); self.0.phase = Phase::Method; Ok({ - let __pending_field_receiver = self - .0 - .iterable - .clone() - .ok_or(RuntimeError::Invariant("collection iterable missing"))?; + let __pending_field_receiver = runtime.into_jsvalue( + self.0 + .iterable + .clone() + .ok_or(RuntimeError::Invariant("collection iterable missing"))?, + )?; let __pending_field_key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)); let __pending_field_resume = self; @@ -296,16 +321,17 @@ impl CollectionResume { NativeErrorKind::Type, "value is not iterable", )?; - return self.abrupt(error); + return self.abrupt(runtime, error); }; self.0.phase = Phase::Iterator; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = self - .0 - .iterable - .take() - .ok_or(RuntimeError::Invariant("collection iterable missing"))?; + let __pending_field_receiver = runtime.into_jsvalue( + self.0 + .iterable + .take() + .ok_or(RuntimeError::Invariant("collection iterable missing"))?, + )?; let __pending_field_arguments = Vec::new(); let __pending_field_resume = self; CollectionStep::request_call( @@ -323,12 +349,12 @@ impl CollectionResume { NativeErrorKind::Type, "not an object", )?; - return self.abrupt(error); + return self.abrupt(runtime, error); }; self.0.iterator = Some(iterator.clone()); self.0.phase = Phase::NextMethod; Ok({ - let __pending_field_receiver = Value::Object(iterator); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(iterator))?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?; let __pending_field_resume = self; @@ -341,7 +367,7 @@ impl CollectionResume { } Phase::NextMethod => { self.0.next = value; - self.next_step() + self.next_step(runtime) } Phase::Key(item) => { self.0.phase = Phase::Value { @@ -349,7 +375,7 @@ impl CollectionResume { key: value, }; Ok({ - let __pending_field_receiver = Value::Object(item); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(item))?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Literal2)?; let __pending_field_resume = self; @@ -366,34 +392,37 @@ impl CollectionResume { } else { None }); - self.add(vec![key, value]) + self.add( + runtime, + vec![runtime.into_jsvalue(key)?, runtime.into_jsvalue(value)?], + ) } Phase::Add(entry) => { drop(entry); - self.next_step() + self.next_step(runtime) } _ => Err(RuntimeError::Invariant( "collection completion phase mismatch", )), } } - fn add(self, arguments: Vec) -> Result { - Ok({ - let __pending_field_callable = self - .0 - .adder - .clone() - .ok_or(RuntimeError::Invariant("collection adder missing"))?; - let __pending_field_receiver = Value::Object(self.collection()?); - let __pending_field_arguments = arguments; - let __pending_field_resume = self; - CollectionStep::request_call( - __pending_field_callable, - __pending_field_receiver, - __pending_field_arguments, - __pending_field_resume, - ) - }) + fn add( + self, + runtime: &Runtime, + arguments: Vec, + ) -> Result { + let __pending_field_callable = self + .0 + .adder + .clone() + .ok_or(RuntimeError::Invariant("collection adder missing"))?; + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(self.collection()?))?; + Ok(CollectionStep::request_call( + __pending_field_callable, + __pending_field_receiver, + arguments, + self, + )) } pub(crate) fn next( mut self, @@ -408,26 +437,26 @@ impl CollectionResume { return Ok(CollectionStep::Complete(Completion::Throw(value))); } ObjectIteratorStep::Done => { - return Ok(CollectionStep::Complete(Completion::Return(Value::Object( - self.collection()?, - )))); + return Ok(CollectionStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.collection()?))?, + ))); } - ObjectIteratorStep::Yield(value) => value, + ObjectIteratorStep::Yield(value) => runtime.root_and_release_jsvalue(value)?, }; if !self.0.kind.pairs() { self.0.phase = Phase::Add(None); - return self.add(vec![item]); + return self.add(runtime, vec![runtime.into_jsvalue(item)?]); } let Value::Object(item) = item else { let error = runtime.new_native_error(self.0.realm, NativeErrorKind::Type, "not an object")?; drop(item); self.0.phase = Phase::Add(None); - return self.abrupt(error); + return self.abrupt(runtime, error); }; self.0.phase = Phase::Key(item.clone()); Ok({ - let __pending_field_receiver = Value::Object(item); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(item))?; let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Literal1)?; let __pending_field_resume = self; @@ -448,14 +477,15 @@ pub(crate) fn finish( step = match step { CollectionStep::Complete(result) => return Ok(result), CollectionStep::Prototype { mut resume } => { - let new_target = resume.take_prototype_new_target(); + let new_target = + runtime.root_and_release_jsvalue(resume.take_prototype_new_target())?; resume.prototype( runtime, runtime.constructor_prototype_source(realm, &new_target)?, )? } CollectionStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -464,8 +494,12 @@ pub(crate) fn finish( } CollectionStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let result = runtime.call_internal(realm, &callable, receiver, &arguments)?; drop(arguments); @@ -474,7 +508,7 @@ pub(crate) fn finish( } CollectionStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let method = resume.take_next_method(); + let method = runtime.root_and_release_jsvalue(resume.take_next_method())?; resume.next( runtime, finish_next( @@ -571,24 +605,24 @@ mod owned_tests { #[derive(Default)] struct CollectionStepPending { - prototype_new_target: Option, - read_receiver: Option, + prototype_new_target: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, next_iterator: Option, - next_method: Option, + next_method: Option, close_iterator: Option, close_completion: Option, } impl CollectionStep { - pub(crate) fn request_prototype(new_target: Value, mut resume: CollectionResume) -> Self { + pub(crate) fn request_prototype(new_target: JsValue, mut resume: CollectionResume) -> Self { resume.0.pending_effect.prototype_new_target = Some(new_target); Self::Prototype { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: CollectionResume, ) -> Self { @@ -598,8 +632,8 @@ impl CollectionStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: CollectionResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -609,7 +643,7 @@ impl CollectionStep { } pub(crate) fn request_next( iterator: ObjectRef, - method: Value, + method: JsValue, mut resume: CollectionResume, ) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); @@ -627,14 +661,14 @@ impl CollectionStep { } } impl CollectionResume { - pub(crate) fn take_prototype_new_target(&mut self) -> Value { + pub(crate) fn take_prototype_new_target(&mut self) -> JsValue { self.0 .pending_effect .prototype_new_target .take() .expect("CollectionStep Prototype new_target") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -655,14 +689,14 @@ impl CollectionResume { .take() .expect("CollectionStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("CollectionStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -676,7 +710,7 @@ impl CollectionResume { .take() .expect("CollectionStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.0 .pending_effect .next_method diff --git a/src/engine/builtins/iterator/concat.rs b/src/engine/builtins/iterator/concat.rs index 1a62a77c..7fb60751 100644 --- a/src/engine/builtins/iterator/concat.rs +++ b/src/engine/builtins/iterator/concat.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, HeapError, IteratorConcatData, IteratorConcatItem, ObjectData, RawValue}, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation, NativeInvokeOutcome}, @@ -17,16 +17,18 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - match finish( - self, - realm, - ConcatStep::start(self, realm, ConcatKind::Create, &invocation, arguments)?, - )? { - NativeInvokeOutcome::Completion(result) => Ok(result), - NativeInvokeOutcome::IteratorNextRaw { .. } => { - Err(RuntimeError::Invariant("concat creation returned raw next")) + self.dispatch_borrowed_invocation(invocation, |invocation| { + match finish( + self, + realm, + ConcatStep::start(self, realm, ConcatKind::Create, invocation, arguments)?, + )? { + NativeInvokeOutcome::Completion(result) => Ok(result), + NativeInvokeOutcome::IteratorNextRaw { .. } => { + Err(RuntimeError::Invariant("concat creation returned raw next")) + } } - } + }) } fn new_iterator_concat( @@ -45,6 +47,13 @@ impl Runtime { })) }) .collect::, RuntimeError>>()?; + // The object retains its own copy edges inside the allocation, so the + // conversions' producer edges are released on every exit below. + let conversion_probes = items + .iter() + .flatten() + .map(|item| item.method.clone()) + .collect::>(); let mut state = self.0.state.borrow_mut(); let shape = state.get_or_create_shape(Some(prototype.object_id()), &[])?; @@ -54,6 +63,10 @@ impl Runtime { Err(error) => { let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } return Err(error); } }; @@ -67,12 +80,19 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } return Err(error.into()); } }; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } Ok(ObjectRef::from_owned_handle(self.clone(), object)) } @@ -127,16 +147,28 @@ impl Runtime { next: &Value, ) -> Result<(), RuntimeError> { let raw = self.raw_property_value(next)?; + // The record retains its own copy edge inside the heap transaction, + // so the conversion's producer edge is released on every exit. + let conversion_edge = raw.conversion_node_edge(); let mut state = self.0.state.borrow_mut(); let retained_atoms = state.retain_raw_value_atoms([&raw])?; let cleanup = match state.heap.set_iterator_concat_next(concat.object_id(), raw) { Ok(cleanup) => cleanup, Err(error) => { state.release_atoms(retained_atoms)?; + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } return Err(error.into()); } }; - state.apply_cleanup(cleanup) + state.apply_cleanup(cleanup)?; + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } + Ok(()) } fn advance_iterator_concat(&self, concat: &ObjectRef) -> Result<(), RuntimeError> { @@ -158,9 +190,12 @@ impl Runtime { ) -> Result { match self.call_iterator_concat_next_raw(realm, invocation)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_iterator_result(realm, value, done)?, + ))?)) + } } } @@ -190,25 +225,27 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - match finish( - self, - realm, - ConcatStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + match finish( self, realm, - ConcatKind::Return, - &invocation, - &NativeArguments { - actual_arg_count: 0, - readable: Vec::new(), - }, - )?, - )? { - NativeInvokeOutcome::Completion(result) => Ok(result), - NativeInvokeOutcome::IteratorNextRaw { .. } => { - Err(RuntimeError::Invariant("concat return returned raw next")) + ConcatStep::start( + self, + realm, + ConcatKind::Return, + invocation, + &NativeArguments { + actual_arg_count: 0, + readable: Vec::new(), + }, + )?, + )? { + NativeInvokeOutcome::Completion(result) => Ok(result), + NativeInvokeOutcome::IteratorNextRaw { .. } => { + Err(RuntimeError::Invariant("concat return returned raw next")) + } } - } + }) } } @@ -299,7 +336,10 @@ impl ConcatStep { )); } // The continuation owns its inputs after the native argv expires. - let inputs = arguments.readable[..arguments.actual_arg_count].to_vec(); + let inputs = arguments.readable[..arguments.actual_arg_count] + .iter() + .map(|value| runtime.root_value(value)) + .collect::, _>>()?; return ConcatResume::input( runtime, realm, @@ -307,11 +347,11 @@ impl ConcatStep { Vec::with_capacity(arguments.actual_arg_count), ); } - let concat = match runtime.iterator_receiver(realm, invocation.clone())? { + let concat = match runtime.iterator_receiver(realm, invocation)? { NativeConversion::Value(concat) => concat, NativeConversion::Throw(value) => { return Ok(Self::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(value), + Completion::Throw(runtime.into_jsvalue(value)?), ))); } }; @@ -319,13 +359,13 @@ impl ConcatStep { NativeConversion::Value(snapshot) => snapshot, NativeConversion::Throw(value) => { return Ok(Self::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(value), + Completion::Throw(runtime.into_jsvalue(value)?), ))); } }; if snapshot.running { return Ok(Self::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "already running", @@ -335,7 +375,7 @@ impl ConcatStep { if matches!(kind, ConcatKind::Return) && snapshot.iterator.is_none() { runtime.clear_iterator_concat(&concat)?; return Ok(Self::Complete(NativeInvokeOutcome::Completion( - Completion::Return(Value::Undefined), + Completion::Return(JsValue::Undefined), ))); } runtime.set_iterator_concat_running(&concat, true)?; @@ -383,12 +423,16 @@ impl ConcatResume { ) -> Result { let Some(input) = remaining.next() else { return Ok(ConcatStep::Complete(NativeInvokeOutcome::Completion( - Completion::Return(Value::Object(runtime.new_iterator_concat(realm, &inputs)?)), + Completion::Return( + runtime.into_jsvalue(Value::Object( + runtime.new_iterator_concat(realm, &inputs)?, + ))?, + ), ))); }; let Value::Object(current) = input else { return Ok(ConcatStep::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an object", @@ -442,7 +486,7 @@ impl ConcatResume { }; if snapshot.index >= snapshot.items.len() { return self.complete(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }); } @@ -460,9 +504,8 @@ impl ConcatResume { "Iterator Concat current input was already released", ))?; let iterable = ObjectRef::from_borrowed_handle(runtime.clone(), item.iterable)?; - let callable = match runtime - .iterator_callable_value(self.0.realm, runtime.root_raw_value(&item.method)?)? - { + let method = runtime.root_raw_value(&item.method)?; + let callable = match runtime.iterator_callable_value(self.0.realm, &method)? { NativeConversion::Value(callable) => callable, NativeConversion::Throw(_) => { return Err(RuntimeError::Invariant( @@ -473,7 +516,7 @@ impl ConcatResume { self.0.phase = ConcatPhase::Iterator; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Object(iterable); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(iterable))?; let __pending_field_resume = self; ConcatStep::request_call( __pending_field_callable, @@ -513,7 +556,7 @@ impl ConcatResume { self.0.phase = ConcatPhase::Next; Ok({ let __pending_field_iterator = iterator; - let __pending_field_method = method; + let __pending_field_method = runtime.into_jsvalue(method)?; let __pending_field_resume = self; ConcatStep::request_next( __pending_field_iterator, @@ -528,7 +571,7 @@ impl ConcatResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return self.complete(NativeInvokeOutcome::Completion(Completion::Throw(value))); } @@ -540,17 +583,18 @@ impl ConcatResume { current, } => { if let NativeConversion::Throw(value) = - runtime.iterator_callable_value(self.0.realm, value.clone())? + runtime.iterator_callable_value(self.0.realm, &value)? { - return self - .complete(NativeInvokeOutcome::Completion(Completion::Throw(value))); + return self.complete(NativeInvokeOutcome::Completion(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } inputs.push((current, value)); Self::input(runtime, self.0.realm, remaining, inputs) } ConcatPhase::Iterator => { let Value::Object(iterator) = value else { - let error = runtime.new_native_error( + let error = runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", @@ -565,7 +609,7 @@ impl ConcatResume { runtime.set_iterator_concat_next(self.concat()?, &value)?; Ok({ let __pending_field_iterator = iterator; - let __pending_field_method = value; + let __pending_field_method = runtime.into_jsvalue(value)?; let __pending_field_resume = self; ConcatStep::request_next( __pending_field_iterator, @@ -580,17 +624,18 @@ impl ConcatResume { .as_mut() .ok_or(RuntimeError::Invariant("concat return owner missing"))? .clear = true; - let callable = match runtime.iterator_callable_value(self.0.realm, value)? { + let callable = match runtime.iterator_callable_value(self.0.realm, &value)? { NativeConversion::Value(callable) => callable, NativeConversion::Throw(value) => { - return self - .complete(NativeInvokeOutcome::Completion(Completion::Throw(value))); + return self.complete(NativeInvokeOutcome::Completion(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.phase = ConcatPhase::ReturnResult; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Object(iterator); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(iterator))?; let __pending_field_resume = self; ConcatStep::request_call( __pending_field_callable, @@ -599,9 +644,9 @@ impl ConcatResume { ) }) } - ConcatPhase::ReturnResult => { - self.complete(NativeInvokeOutcome::Completion(Completion::Return(value))) - } + ConcatPhase::ReturnResult => self.complete(NativeInvokeOutcome::Completion( + Completion::Return(runtime.into_jsvalue(value)?), + )), ConcatPhase::Next => Err(RuntimeError::Invariant("concat next received completion")), } } @@ -645,7 +690,7 @@ pub(crate) fn finish( } ConcatStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &[])?, @@ -653,7 +698,7 @@ pub(crate) fn finish( } ConcatStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let method = resume.take_next_method(); + let method = runtime.root_and_release_jsvalue(resume.take_next_method())?; resume.next( runtime, super::step::finish_next( @@ -672,9 +717,9 @@ struct ConcatStepPending { read_object: Option, read_key: Option, call_callable: Option, - call_receiver: Option, + call_receiver: Option, next_iterator: Option, - next_method: Option, + next_method: Option, } impl ConcatStep { pub(crate) fn request_read( @@ -688,7 +733,7 @@ impl ConcatStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, + receiver: JsValue, mut resume: ConcatResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -697,7 +742,7 @@ impl ConcatStep { } pub(crate) fn request_next( iterator: ObjectRef, - method: Value, + method: JsValue, mut resume: ConcatResume, ) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); @@ -727,7 +772,7 @@ impl ConcatResume { .take() .expect("ConcatStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver @@ -741,7 +786,7 @@ impl ConcatResume { .take() .expect("ConcatStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.0 .pending_effect .next_method diff --git a/src/engine/builtins/iterator/constructor.rs b/src/engine/builtins/iterator/constructor.rs index f9d67632..ae62c6a1 100644 --- a/src/engine/builtins/iterator/constructor.rs +++ b/src/engine/builtins/iterator/constructor.rs @@ -4,7 +4,7 @@ use crate::engine::{ builtins::native::NativeFunctionId, heap::{ContextId, ObjectPayload}, object::{CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{ConstructorPrototypeSource, NativeArguments, NativeInvocation}, @@ -13,7 +13,7 @@ use crate::engine::{ pub(crate) enum ConstructorStep { Complete(Completion), Prototype { - new_target: Value, + new_target: JsValue, resume: ConstructorResume, }, } @@ -24,32 +24,44 @@ impl ConstructorStep { realm: ContextId, invocation: &NativeInvocation, ) -> Result { - let new_target = match invocation { - NativeInvocation::Construct { - new_target: Value::Object(object), - } => object, - NativeInvocation::Construct { .. } | NativeInvocation::Call { .. } => { - return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( - realm, - NativeErrorKind::Type, - "constructor requires 'new'", - )?, - ))); - } - NativeInvocation::Getter { .. } | NativeInvocation::Setter { .. } => { + let NativeInvocation::Construct { new_target } = invocation else { + if matches!( + invocation, + NativeInvocation::Getter { .. } | NativeInvocation::Setter { .. } + ) { return Err(RuntimeError::Invariant( "Iterator constructor received an accessor invocation", )); } + return Ok(Self::Complete(Completion::Throw( + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "constructor requires 'new'", + )?, + ))); }; + let new_target_value = runtime.dup_jsvalue(new_target)?; + let JsValue::Object(new_target_id) = &new_target_value else { + return Ok(Self::Complete(Completion::Throw( + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "constructor requires 'new'", + )?, + ))); + }; + let new_target = crate::engine::object::ObjectRef::from_borrowed_handle( + runtime.clone(), + *new_target_id, + )?; let native_iterator = { let state = runtime.0.state.borrow(); matches!(&state.heap.object(new_target.object_id())?.payload, ObjectPayload::NativeFunction { data, .. } if data.target == NativeFunctionId::IteratorConstructor) }; if native_iterator { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "abstract class not constructable", @@ -57,7 +69,7 @@ impl ConstructorStep { ))); } Ok(Self::Prototype { - new_target: Value::Object(new_target.clone()), + new_target: new_target_value, resume: ConstructorResume, }) } @@ -93,20 +105,25 @@ impl ConstructorStep { }; if arguments.actual_arg_count == 0 { let constructor = runtime.iterator_realm_data(defining_realm)?.constructor; - return Ok(Self::Complete(Completion::Return(Value::Object( - ObjectRef::from_borrowed_handle(runtime.clone(), constructor)?, - )))); + return Ok(Self::Complete(Completion::Return(runtime.into_jsvalue( + Value::Object(ObjectRef::from_borrowed_handle( + runtime.clone(), + constructor, + )?), + )?))); } - let Some(Value::Object(value)) = arguments.readable.first() else { + let Some(JsValue::Object(value_id)) = arguments.readable.first() else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; - let Value::Object(receiver) = this_value else { + let value = ObjectRef::from_borrowed_handle(runtime.clone(), *value_id)?; + let JsValue::Object(receiver_id) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; + let receiver = ObjectRef::from_borrowed_handle(runtime.clone(), *receiver_id)?; let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Constructor)?; let descriptor = OrdinaryPropertyDescriptor { @@ -116,10 +133,10 @@ impl ConstructorStep { configurable: DescriptorField::Present(true), ..OrdinaryPropertyDescriptor::new() }; - let completion = if runtime.define_own_property(receiver, &key, &descriptor)? { - Completion::Return(Value::Undefined) + let completion = if runtime.define_own_property(&receiver, &key, &descriptor)? { + Completion::Return(JsValue::Undefined) } else { - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot define property", @@ -136,7 +153,9 @@ impl ConstructorResume { ) -> Result { let prototype = match reply { NativeConversion::Throw(value) => { - return Ok(ConstructorStep::Complete(Completion::Throw(value))); + return Ok(ConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(ConstructorPrototypeSource::Explicit(prototype)) => prototype, NativeConversion::Value(ConstructorPrototypeSource::Realm(realm)) => { @@ -151,7 +170,7 @@ impl ConstructorResume { } }; Ok(ConstructorStep::Complete(Completion::Return( - Value::Object(runtime.new_iterator_object(&prototype)?), + runtime.into_jsvalue(Value::Object(runtime.new_iterator_object(&prototype)?))?, ))) } } @@ -163,10 +182,13 @@ pub(crate) fn finish( loop { step = match step { ConstructorStep::Complete(result) => return Ok(result), - ConstructorStep::Prototype { new_target, resume } => resume.prototype( - runtime, - runtime.constructor_prototype_source(realm, &new_target)?, - )?, + ConstructorStep::Prototype { new_target, resume } => { + let new_target = runtime.root_and_release_jsvalue(new_target)?; + resume.prototype( + runtime, + runtime.constructor_prototype_source(realm, &new_target)?, + )? + } }; } } diff --git a/src/engine/builtins/iterator/consume.rs b/src/engine/builtins/iterator/consume.rs index 8d3ee9fd..8a5106b8 100644 --- a/src/engine/builtins/iterator/consume.rs +++ b/src/engine/builtins/iterator/consume.rs @@ -10,7 +10,7 @@ use crate::engine::{ builtins::native::NativeFunctionId, heap::{ContextId, IteratorConsumerKind}, object::{CallableRef, ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -86,40 +86,34 @@ impl ConsumeStep { invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { - let source = match runtime.iterator_receiver(realm, invocation.clone())? { + let source = match runtime.iterator_receiver(realm, invocation)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let callback = if matches!(kind, ConsumeKind::Array) { None } else { - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Iterator consumer callback was not padded", - ))?; - match runtime.iterator_callable_value(realm, value)? { + let value = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Iterator consumer callback was not padded"), + )?)?; + match runtime.iterator_callable_value(realm, &value)? { NativeConversion::Value(callback) => Some(callback), NativeConversion::Throw(value) => { return Ok(Self::Close { iterator: source, - completion: Completion::Throw(value), + completion: Completion::Throw(runtime.into_jsvalue(value)?), }); } } }; let accumulator = if matches!(kind, ConsumeKind::Reduce) && arguments.actual_arg_count > 1 { - Some( - arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Iterator reduce initial value disappeared", - ))?, - ) + Some(runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("Iterator reduce initial value disappeared"), + )?)?) } else { None }; @@ -154,17 +148,17 @@ impl ConsumeResume { completion, } } - fn next_step(mut self) -> ConsumeStep { + fn next_step(mut self, runtime: &Runtime) -> Result { self.0.phase = Phase::Next; { let __pending_field_iterator = self.0.source.clone(); - let __pending_field_method = self.0.next.clone(); + let __pending_field_method = runtime.into_jsvalue(self.0.next.clone())?; let __pending_field_resume = self; - ConsumeStep::request_next( + Ok(ConsumeStep::request_next( __pending_field_iterator, __pending_field_method, __pending_field_resume, - ) + )) } } pub(crate) fn resume( @@ -173,7 +167,7 @@ impl ConsumeResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok( if matches!(self.0.phase, Phase::Callback(_)) @@ -192,7 +186,7 @@ impl ConsumeResume { if matches!(self.0.kind, ConsumeKind::Array) { self.0.array = Some(runtime.new_array(self.0.realm)?); } - Ok(self.next_step()) + Ok(self.next_step(runtime)?) } Phase::Callback(item) => { self.0.index = self.0.index.wrapping_add(1); @@ -218,9 +212,9 @@ impl ConsumeResume { } }; Ok(if let Some(value) = early { - self.close(Completion::Return(value)) + self.close(Completion::Return(runtime.into_jsvalue(value)?)) } else { - self.next_step() + self.next_step(runtime)? }) } Phase::Next => Err(RuntimeError::Invariant( @@ -258,7 +252,7 @@ impl ConsumeResume { ConsumeKind::Reduce => match self.0.accumulator.take() { Some(value) => value, None => { - let error = runtime.new_native_error( + let error = runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "empty iterator", @@ -267,9 +261,11 @@ impl ConsumeResume { } }, }; - return Ok(ConsumeStep::Complete(Completion::Return(value))); + return Ok(ConsumeStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))); } - ObjectIteratorStep::Yield(value) => value, + ObjectIteratorStep::Yield(value) => runtime.root_and_release_jsvalue(value)?, }; if matches!(self.0.kind, ConsumeKind::Array) { // This unpublished fresh Array has no callback-capable definition; @@ -285,7 +281,9 @@ impl ConsumeResume { self.0.index as u32, item, )? { - return Ok(ConsumeStep::Complete(Completion::Throw(value))); + return Ok(ConsumeStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } self.0.index = u32::try_from(self.0.index) .ok() @@ -294,25 +292,28 @@ impl ConsumeResume { .ok_or_else(|| { RuntimeError::Engine(Error::new(ErrorKind::Range, "invalid array length")) })?; - return Ok(self.next_step()); + return self.next_step(runtime); } if matches!(self.0.kind, ConsumeKind::Reduce) && self.0.accumulator.is_none() { self.0.accumulator = Some(item); self.0.index = 1; - return Ok(self.next_step()); + return self.next_step(runtime); } let callable = self.0.callback.clone().ok_or(RuntimeError::Invariant( "Iterator consumer callback missing", ))?; let arguments = match self.0.kind { ConsumeKind::Reduce => vec![ - self.0.accumulator.take().ok_or(RuntimeError::Invariant( - "Iterator reduce accumulator missing", - ))?, - item.clone(), - Value::number(self.0.index as f64), + runtime.into_jsvalue(self.0.accumulator.take().ok_or( + RuntimeError::Invariant("Iterator reduce accumulator missing"), + )?)?, + runtime.into_jsvalue(item.clone())?, + JsValue::Float(self.0.index as f64), + ], + _ => vec![ + runtime.into_jsvalue(item.clone())?, + JsValue::Float(self.0.index as f64), ], - _ => vec![item.clone(), Value::number(self.0.index as f64)], }; self.0.phase = Phase::Callback(item); Ok({ @@ -355,7 +356,11 @@ pub(crate) fn finish( } ConsumeStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let arguments = resume.take_call_arguments(); + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, Value::Undefined, &arguments)?, @@ -363,7 +368,7 @@ pub(crate) fn finish( } ConsumeStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let method = resume.take_next_method(); + let method = runtime.root_and_release_jsvalue(resume.take_next_method())?; resume.next( runtime, finish_next( @@ -382,9 +387,9 @@ struct ConsumeStepPending { read_object: Option, read_key: Option, next_iterator: Option, - next_method: Option, + next_method: Option, call_callable: Option, - call_arguments: Option>, + call_arguments: Option>, } impl ConsumeStep { pub(crate) fn request_read( @@ -398,7 +403,7 @@ impl ConsumeStep { } pub(crate) fn request_next( iterator: ObjectRef, - method: Value, + method: JsValue, mut resume: ConsumeResume, ) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); @@ -407,7 +412,7 @@ impl ConsumeStep { } pub(crate) fn request_call( callable: CallableRef, - arguments: Vec, + arguments: Vec, mut resume: ConsumeResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -437,7 +442,7 @@ impl ConsumeResume { .take() .expect("ConsumeStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.0 .pending_effect .next_method @@ -451,7 +456,7 @@ impl ConsumeResume { .take() .expect("ConsumeStep Call callable") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/iterator/create.rs b/src/engine/builtins/iterator/create.rs index 122b5281..59667027 100644 --- a/src/engine/builtins/iterator/create.rs +++ b/src/engine/builtins/iterator/create.rs @@ -7,7 +7,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, IteratorHelperKind}, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -16,7 +16,7 @@ use crate::engine::{ pub(crate) enum CreateStep { Complete(Completion), Number { - value: Value, + value: JsValue, resume: CreateResume, }, Read { @@ -61,17 +61,17 @@ impl CreateStep { invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { - let source = match runtime.iterator_receiver(realm, invocation.clone())? { + let source = match runtime.iterator_receiver(realm, invocation)? { NativeConversion::Value(source) => source, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Iterator helper argument was not padded", - ))?; + let argument = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Iterator helper argument was not padded"), + )?)?; let mut resume = CreateResume(Box::new(CreateResumeState { realm, source, @@ -85,21 +85,22 @@ impl CreateStep { resume, }); } + let argument_value = runtime.root_and_release_jsvalue(argument)?; if let NativeConversion::Throw(value) = - runtime.iterator_callable_value(realm, argument.clone())? + runtime.iterator_callable_value(realm, &argument_value)? { - return Ok(resume.close(value)); + return resume.close(runtime, value); } - resume.callback = argument; + resume.callback = argument_value; resume.read(runtime) } } impl CreateResume { - fn close(self, value: Value) -> CreateStep { - CreateStep::Close { + fn close(self, runtime: &Runtime, value: Value) -> Result { + Ok(CreateStep::Close { iterator: self.0.source, - completion: Completion::Throw(value), - } + completion: Completion::Throw(runtime.into_jsvalue(value)?), + }) } fn read(self, runtime: &Runtime) -> Result { Ok(CreateStep::Read { @@ -115,7 +116,7 @@ impl CreateResume { ) -> Result { let number = match reply { NativeConversion::Value(number) => number, - NativeConversion::Throw(value) => return Ok(self.close(value)), + NativeConversion::Throw(value) => return self.close(runtime, value), }; let count = if number == f64::INFINITY { (1_i64 << 53) - 1 @@ -137,7 +138,11 @@ impl CreateResume { _reply: Completion, ) -> Result { Ok(CreateStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Range, "must be positive")?, + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Range, + "must be positive", + )?, ))) } pub(crate) fn resume( @@ -146,17 +151,22 @@ impl CreateResume { reply: Completion, ) -> Result { match reply { - Completion::Throw(value) => Ok(self.close(value)), - Completion::Return(next) => Ok(CreateStep::Complete(Completion::Return( - Value::Object(runtime.new_iterator_helper( - self.0.realm, - &self.0.source, - &next, - &self.0.callback, - self.0.count, - self.0.kind, - )?), - ))), + Completion::Throw(value) => { + self.close(runtime, runtime.root_and_release_jsvalue(value)?) + } + Completion::Return(next) => { + let next = runtime.root_and_release_jsvalue(next)?; + Ok(CreateStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(runtime.new_iterator_helper( + self.0.realm, + &self.0.source, + &next, + &self.0.callback, + self.0.count, + self.0.kind, + )?))?, + ))) + } } } } @@ -169,6 +179,7 @@ pub(crate) fn finish( step = match step { CreateStep::Complete(result) => return Ok(result), CreateStep::Number { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } CreateStep::Read { @@ -187,7 +198,7 @@ pub(crate) fn finish( runtime, realm, iterator, - Completion::Throw(Value::Undefined), + Completion::Throw(JsValue::Undefined), )?, )?; resume.invalid_count(runtime, result)? diff --git a/src/engine/builtins/iterator/entry.rs b/src/engine/builtins/iterator/entry.rs index 5f22da27..30361852 100644 --- a/src/engine/builtins/iterator/entry.rs +++ b/src/engine/builtins/iterator/entry.rs @@ -8,7 +8,7 @@ use crate::engine::object::{ DescriptorField, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -22,7 +22,7 @@ impl Runtime { "Iterator.prototype iterator did not receive a generic invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } pub(crate) fn call_iterator_prototype_to_string_tag_getter( @@ -34,9 +34,9 @@ impl Runtime { "Iterator.prototype toStringTag getter received the wrong native invocation", )); }; - Ok(Completion::Return(Value::String(JsString::from_static( - "Iterator", - )))) + Ok(Completion::Return(self.into_jsvalue(Value::String( + JsString::from_static("Iterator"), + ))?)) } pub(crate) fn call_iterator_prototype_to_string_tag_setter( @@ -45,11 +45,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - finish_tag( - self, - realm, - TagSetterStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + finish_tag( + self, + realm, + TagSetterStep::start(self, realm, invocation, arguments)?, + ) + }) } } @@ -75,6 +77,7 @@ const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct TagSetterResumeState { pending_effect: TagSetterStepPending, realm: ContextId, + runtime: Runtime, receiver: crate::engine::object::ObjectRef, key: PropertyKey, value: Value, @@ -91,19 +94,17 @@ impl TagSetterStep { "Iterator.prototype toStringTag setter received the wrong native invocation", )); }; - let Value::Object(receiver) = this_value else { + let JsValue::Object(receiver_id) = this_value else { return Err(RuntimeError::Engine(Error::new( ErrorKind::Type, "not an object", ))); }; - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Iterator.prototype toStringTag setter argv was not padded", - ))?; + let receiver = + crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), *receiver_id)?; + let value = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Iterator.prototype toStringTag setter argv was not padded"), + )?)?; let iterator_prototype = runtime .0 .state @@ -124,6 +125,7 @@ impl TagSetterStep { let __pending_field_resume = TagSetterResume(Box::new(TagSetterResumeState { pending_effect: TagSetterStepPending::default(), realm, + runtime: runtime.clone(), receiver: receiver.clone(), key, value, @@ -142,11 +144,13 @@ impl TagSetterResume { reply: NativeConversion, ) -> Result { match reply { - NativeConversion::Throw(value) => Ok(TagSetterStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(TagSetterStep::Complete(Completion::Throw( + self.0.runtime.into_jsvalue(value)?, + ))), NativeConversion::Value(true) => Ok({ let __pending_field_object = self.0.receiver.clone(); let __pending_field_key = self.0.key.clone(); - let __pending_field_value = self.0.value.clone(); + let __pending_field_value = self.0.runtime.into_jsvalue(self.0.value.clone())?; let __pending_field_resume = self; TagSetterStep::request_set( __pending_field_object, @@ -182,10 +186,10 @@ impl TagSetterResume { ) -> Result { Ok(TagSetterStep::Complete(match reply { NativeConversion::Value(InternalDefineResult::Defined) => { - Completion::Return(Value::Undefined) + Completion::Return(JsValue::Undefined) } NativeConversion::Value(InternalDefineResult::RejectedProxyTrap) => { - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "proxy: defineProperty exception", @@ -199,13 +203,13 @@ impl TagSetterResume { } else { "property is not configurable" }; - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, message, )?) } - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), })) } pub(crate) fn set( @@ -215,8 +219,8 @@ impl TagSetterResume { ) -> Result { Ok(TagSetterStep::Complete( match runtime.finish_set_property_or_throw(self.0.realm, &self.0.key, reply)? { - Some(value) => Completion::Throw(value), - None => Completion::Return(Value::Undefined), + Some(value) => Completion::Throw(runtime.into_jsvalue(value)?), + None => Completion::Return(JsValue::Undefined), }, )) } @@ -246,7 +250,7 @@ pub(crate) fn finish_tag( TagSetterStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; resume.set( runtime, runtime.internal_set( @@ -271,7 +275,7 @@ struct TagSetterStepPending { define_descriptor: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, } impl TagSetterStep { pub(crate) fn request_own( @@ -297,7 +301,7 @@ impl TagSetterStep { pub(crate) fn request_set( object: crate::engine::object::ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: TagSetterResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -356,7 +360,7 @@ impl TagSetterResume { .take() .expect("TagSetterStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/builtins/iterator/from.rs b/src/engine/builtins/iterator/from.rs index 24fd766e..b5a85a36 100644 --- a/src/engine/builtins/iterator/from.rs +++ b/src/engine/builtins/iterator/from.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -51,16 +51,12 @@ impl FromStep { "Iterator.from did not receive a generic invocation", )); } - let input = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Iterator.from argument was not padded", - ))?; + let input = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Iterator.from argument was not padded"), + )?)?; if !matches!(input, Value::Object(_) | Value::String(_)) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "Iterator.from called on non-object", @@ -68,16 +64,15 @@ impl FromStep { ))); } Ok({ - let __pending_field_receiver = input.clone(); let __pending_field_key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)); let __pending_field_resume = FromResume(Box::new(FromResumeState { pending_effect: FromStepPending::default(), realm, - phase: Phase::Method(input), + phase: Phase::Method(input.clone()), })); Self::request_read( - __pending_field_receiver, + runtime.into_jsvalue(input)?, __pending_field_key, __pending_field_resume, ) @@ -88,7 +83,7 @@ impl FromResume { fn next(mut self, runtime: &Runtime, iterator: Value) -> Result { self.0.phase = Phase::Next(iterator.clone()); Ok({ - let __pending_field_receiver = iterator; + let __pending_field_receiver = runtime.into_jsvalue(iterator)?; let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?; let __pending_field_resume = self; @@ -105,23 +100,27 @@ impl FromResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(FromStep::Complete(Completion::Throw(value))), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(FromStep::Complete(Completion::Throw(value))); + } }; match std::mem::replace(&mut self.0.phase, Phase::Iterator) { Phase::Method(input) => { if matches!(value, Value::Undefined | Value::Null) { return self.next(runtime, input); } - let callable = match runtime.iterator_callable_value(self.0.realm, value)? { + let callable = match runtime.iterator_callable_value(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(FromStep::Complete(Completion::Throw(value))); + return Ok(FromStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = input; + let __pending_field_receiver = runtime.into_jsvalue(input)?; let __pending_field_resume = self; FromStep::request_call( __pending_field_callable, @@ -133,7 +132,7 @@ impl FromResume { Phase::Iterator => { if !matches!(value, Value::Object(_)) { return Ok(FromStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", @@ -153,7 +152,7 @@ impl FromResume { }; Ok({ let __pending_field_constructor = constructor; - let __pending_field_value = iterator; + let __pending_field_value = runtime.into_jsvalue(iterator)?; let __pending_field_resume = self; FromStep::request_instance( __pending_field_constructor, @@ -163,11 +162,11 @@ impl FromResume { }) } Phase::Instance { iterator, next } => Ok(FromStep::Complete(Completion::Return( - if runtime.value_to_boolean(&value)? { + runtime.into_jsvalue(if runtime.value_to_boolean(&value)? { iterator } else { Value::Object(runtime.new_iterator_wrap(self.0.realm, &iterator, &next)?) - }, + })?, ))), } } @@ -181,7 +180,7 @@ pub(crate) fn finish( step = match step { FromStep::Complete(result) => return Ok(result), FromStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -190,7 +189,7 @@ pub(crate) fn finish( } FromStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &[])?, @@ -198,7 +197,7 @@ pub(crate) fn finish( } FromStep::Instance { mut resume } => { let constructor = resume.take_instance_constructor(); - let value = resume.take_instance_value(); + let value = runtime.root_and_release_jsvalue(resume.take_instance_value())?; resume.resume( runtime, runtime.ordinary_is_instance_of(realm, &constructor, value)?, @@ -210,22 +209,26 @@ pub(crate) fn finish( #[derive(Default)] struct FromStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, + call_receiver: Option, instance_constructor: Option, - instance_value: Option, + instance_value: Option, } impl FromStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: FromResume) -> Self { + pub(crate) fn request_read( + receiver: JsValue, + key: PropertyKey, + mut resume: FromResume, + ) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, + receiver: JsValue, mut resume: FromResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -234,7 +237,7 @@ impl FromStep { } pub(crate) fn request_instance( constructor: CallableRef, - value: Value, + value: JsValue, mut resume: FromResume, ) -> Self { resume.0.pending_effect.instance_constructor = Some(constructor); @@ -243,7 +246,7 @@ impl FromStep { } } impl FromResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -264,7 +267,7 @@ impl FromResume { .take() .expect("FromStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver @@ -278,7 +281,7 @@ impl FromResume { .take() .expect("FromStep Instance constructor") } - pub(crate) fn take_instance_value(&mut self) -> Value { + pub(crate) fn take_instance_value(&mut self) -> JsValue { self.0 .pending_effect .instance_value diff --git a/src/engine/builtins/iterator/helper.rs b/src/engine/builtins/iterator/helper.rs index 0ad7414f..8619a7fb 100644 --- a/src/engine/builtins/iterator/helper.rs +++ b/src/engine/builtins/iterator/helper.rs @@ -7,7 +7,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, HeapError, IteratorHelperKind, IteratorResumeKind}, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeInvocation}, }; pub(crate) enum HelperResumeStep { @@ -61,6 +61,7 @@ impl std::ops::DerefMut for HelperResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct HelperResumeState { + runtime: Runtime, pending_effect: HelperResumeStepPending, realm: ContextId, guard: RunningHelper, @@ -75,6 +76,24 @@ pub(crate) struct HelperResumeState { method: Value, phase: Phase, } +impl Drop for HelperResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.next_method.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} enum Phase { Method, OuterNext { dropping: bool }, @@ -94,9 +113,13 @@ impl HelperResumeStep { mode: IteratorResumeKind, invocation: &NativeInvocation, ) -> Result { - let helper = match runtime.iterator_receiver(realm, invocation.clone())? { + let helper = match runtime.iterator_receiver(realm, invocation)? { NativeConversion::Value(helper) => helper, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let state_result = { runtime @@ -110,7 +133,7 @@ impl HelperResumeStep { Ok(state) => state, Err(HeapError::Invariant(_)) => { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an Iterator Helper", @@ -121,7 +144,7 @@ impl HelperResumeStep { }; if state.executing { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot invoke a running iterator", @@ -129,9 +152,9 @@ impl HelperResumeStep { ))); } if state.done { - return Ok(Self::Complete(Completion::Return(Value::Object( - runtime.new_iterator_result(realm, Value::Undefined, true)?, - )))); + return Ok(Self::Complete(Completion::Return(runtime.into_jsvalue( + Value::Object(runtime.new_iterator_result(realm, Value::Undefined, true)?), + )?))); } runtime .0 @@ -152,6 +175,7 @@ impl HelperResumeStep { .map(|inner| ObjectRef::from_borrowed_handle(runtime.clone(), inner)) .transpose()?; let resume = HelperResume(Box::new(HelperResumeState { + runtime: runtime.clone(), pending_effect: HelperResumeStepPending::default(), realm, guard, @@ -180,7 +204,11 @@ impl HelperResume { .guard .finish(done || self.0.mode == IteratorResumeKind::Return)?; Ok(HelperResumeStep::Complete(Completion::Return( - Value::Object(runtime.new_iterator_result(self.0.realm, value, done)?), + runtime.into_jsvalue(Value::Object(runtime.new_iterator_result( + self.0.realm, + value, + done, + )?))?, ))) } fn fail( @@ -193,7 +221,7 @@ impl HelperResume { self.0.phase = Phase::CloseOuter; return Ok({ let __pending_field_iterator = self.0.source.clone(); - let __pending_field_completion = Completion::Throw(value); + let __pending_field_completion = Completion::Throw(runtime.into_jsvalue(value)?); let __pending_field_resume = self; HelperResumeStep::request_close( __pending_field_iterator, @@ -205,8 +233,9 @@ impl HelperResume { let done = self.0.mode == IteratorResumeKind::Return || (self.0.kind == IteratorHelperKind::Take && self.0.original_count == 0); self.0.guard.finish(done)?; - let _ = runtime; - Ok(HelperResumeStep::Complete(Completion::Throw(value))) + Ok(HelperResumeStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))) } fn begin(mut self, runtime: &Runtime) -> Result { if self.0.kind == IteratorHelperKind::FlatMap && self.0.inner.is_some() { @@ -216,7 +245,7 @@ impl HelperResume { self.0.phase = Phase::CloseTake; return Ok({ let __pending_field_iterator = self.0.source.clone(); - let __pending_field_completion = Completion::Return(Value::Undefined); + let __pending_field_completion = Completion::Return(JsValue::Undefined); let __pending_field_resume = self; HelperResumeStep::request_close( __pending_field_iterator, @@ -264,7 +293,7 @@ impl HelperResume { self.0.phase = Phase::OuterNext { dropping }; Ok({ let __pending_field_iterator = self.0.source.clone(); - let __pending_field_method = self.0.method.clone(); + let __pending_field_method = runtime.into_jsvalue(self.0.method.clone())?; let __pending_field_resume = self; HelperResumeStep::request_next( __pending_field_iterator, @@ -307,7 +336,7 @@ impl HelperResume { self.0.phase = Phase::CloseInner { original }; Ok({ let __pending_field_iterator = iterator; - let __pending_field_completion = Completion::Return(Value::Undefined); + let __pending_field_completion = Completion::Return(JsValue::Undefined); let __pending_field_resume = self; HelperResumeStep::request_close( __pending_field_iterator, @@ -323,20 +352,27 @@ impl HelperResume { ) -> Result { if matches!(self.0.phase, Phase::InnerNext) { return match reply { - ObjectIteratorStep::Yield(value) => self.done(runtime, value, false), + ObjectIteratorStep::Yield(value) => { + self.done(runtime, runtime.root_and_release_jsvalue(value)?, false) + } ObjectIteratorStep::Done => self.close_inner(None), - ObjectIteratorStep::Throw(value) => self.close_inner(Some(value)), + ObjectIteratorStep::Throw(value) => { + self.close_inner(Some(runtime.root_and_release_jsvalue(value)?)) + } }; } - let Phase::OuterNext { dropping } = self.0.phase else { + let Phase::OuterNext { dropping } = std::mem::replace(&mut self.0.phase, Phase::Method) + else { return Err(RuntimeError::Invariant( "helper iterator reply has wrong phase", )); }; let value = match reply { - ObjectIteratorStep::Throw(value) => return self.fail(runtime, value, false), + ObjectIteratorStep::Throw(value) => { + return self.fail(runtime, runtime.root_and_release_jsvalue(value)?, false); + } ObjectIteratorStep::Done => return self.done(runtime, Value::Undefined, true), - ObjectIteratorStep::Yield(value) => value, + ObjectIteratorStep::Yield(value) => runtime.root_and_release_jsvalue(value)?, }; if dropping { if self.0.mode == IteratorResumeKind::Return { @@ -352,23 +388,23 @@ impl HelperResume { { return self.done(runtime, value, false); } - let callable = - match runtime.iterator_callable_value(self.0.realm, self.0.callback.clone())? { - NativeConversion::Value(callback) => callback, - NativeConversion::Throw(_) => { - return Err(RuntimeError::Invariant( - "Iterator Helper callback lost its callable brand", - )); - } - }; + let callable = match runtime.iterator_callable_value(self.0.realm, &self.0.callback)? { + NativeConversion::Value(callback) => callback, + NativeConversion::Throw(_) => { + return Err(RuntimeError::Invariant( + "Iterator Helper callback lost its callable brand", + )); + } + }; let index = self.0.count; self.0.count = self.0.count.wrapping_add(1); runtime.set_helper_count(&self.0.guard.helper, self.0.count)?; self.0.phase = Phase::Callback(value.clone()); Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; - let __pending_field_arguments = vec![value, Value::number(index as f64)]; + let __pending_field_receiver = JsValue::Undefined; + let __pending_field_arguments = + vec![runtime.into_jsvalue(value)?, JsValue::Float(index as f64)]; let __pending_field_resume = self; HelperResumeStep::request_call( __pending_field_callable, @@ -390,7 +426,7 @@ impl HelperResume { return if let Some(original) = original { let value = match reply { Completion::Return(_) => original, - Completion::Throw(value) => value, + Completion::Throw(value) => runtime.root_and_release_jsvalue(value)?, }; self.fail(runtime, value, true) } else { @@ -407,8 +443,9 @@ impl HelperResume { phase: Phase, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { + let value = runtime.root_and_release_jsvalue(value)?; return match phase { Phase::InnerMethod => self.close_inner(Some(value)), Phase::CloseTake | Phase::CloseOuter => self.fail(runtime, value, false), @@ -459,14 +496,16 @@ impl HelperResume { self.0.inner = Some(mapped); return self.inner_method(runtime); } - let callable = match runtime.iterator_callable_value(self.0.realm, value)? { + let callable = match runtime.iterator_callable_value(self.0.realm, &value)? { NativeConversion::Value(callable) => callable, - NativeConversion::Throw(value) => return self.fail(runtime, value, true), + NativeConversion::Throw(value) => { + return self.fail(runtime, value, true); + } }; self.0.phase = Phase::MappedIterator; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Object(mapped); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(mapped))?; let __pending_field_arguments = Vec::new(); let __pending_field_resume = self; HelperResumeStep::request_call( @@ -503,7 +542,7 @@ impl HelperResume { .inner .clone() .ok_or(RuntimeError::Invariant("flatMap inner missing"))?; - let __pending_field_method = value; + let __pending_field_method = runtime.into_jsvalue(value)?; let __pending_field_resume = self; HelperResumeStep::request_next( __pending_field_iterator, @@ -540,8 +579,12 @@ pub(crate) fn finish( } HelperResumeStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -549,7 +592,7 @@ pub(crate) fn finish( } HelperResumeStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let method = resume.take_next_method(); + let method = runtime.root_and_release_jsvalue(resume.take_next_method())?; resume.next( runtime, finish_next( @@ -598,6 +641,7 @@ mod tests { else { panic!("next expected") }; + let next = runtime.root_and_release_jsvalue(next).unwrap(); let helper = runtime .new_iterator_helper( context.realm, @@ -611,7 +655,7 @@ mod tests { let source_id = source.object_id(); let helper_id = helper.object_id(); let invocation = NativeInvocation::Call { - this_value: Value::Object(helper.clone()), + this_value: runtime.into_jsvalue(Value::Object(helper.clone())).unwrap(), }; let step = HelperResumeStep::start( &runtime, @@ -633,7 +677,12 @@ mod tests { drop(source); drop(next); drop(callback); - drop(invocation); + { + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + } runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(source_id).is_ok()); drop(step); @@ -647,18 +696,26 @@ mod tests { .unwrap() .executing ); + let invocation = NativeInvocation::Call { + this_value: runtime.into_jsvalue(Value::Object(helper.clone())).unwrap(), + }; let step = HelperResumeStep::start( &runtime, context.realm, IteratorResumeKind::Next, - &NativeInvocation::Call { - this_value: Value::Object(helper.clone()), - }, + &invocation, ) .unwrap(); - let Completion::Return(Value::Object(result)) = - finish(&runtime, context.realm, step).unwrap() - else { + { + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + } + let Completion::Return(value) = finish(&runtime, context.realm, step).unwrap() else { + panic!("helper result expected") + }; + let Value::Object(result) = runtime.root_and_release_jsvalue(value).unwrap() else { panic!("helper result expected") }; drop(result); @@ -678,10 +735,10 @@ struct HelperResumeStepPending { read_object: Option, read_key: Option, next_iterator: Option, - next_method: Option, + next_method: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, close_iterator: Option, close_completion: Option, } @@ -697,7 +754,7 @@ impl HelperResumeStep { } pub(crate) fn request_next( iterator: ObjectRef, - method: Value, + method: JsValue, mut resume: HelperResume, ) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); @@ -706,8 +763,8 @@ impl HelperResumeStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: HelperResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -747,7 +804,7 @@ impl HelperResume { .take() .expect("HelperResumeStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.0 .pending_effect .next_method @@ -761,14 +818,14 @@ impl HelperResume { .take() .expect("HelperResumeStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("HelperResumeStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/iterator/mod.rs b/src/engine/builtins/iterator/mod.rs index b7386966..ad382e30 100644 --- a/src/engine/builtins/iterator/mod.rs +++ b/src/engine/builtins/iterator/mod.rs @@ -24,7 +24,7 @@ use crate::engine::object::{ PropertyKey, WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -311,24 +311,27 @@ impl Runtime { .ok_or(RuntimeError::Invariant("realm has no Iterator intrinsics")) } - fn iterator_receiver( + pub(crate) fn iterator_receiver( &self, realm: ContextId, - invocation: NativeInvocation, + invocation: &NativeInvocation, ) -> Result, RuntimeError> { let NativeInvocation::Call { this_value } = invocation else { return Err(RuntimeError::Invariant( "Iterator prototype method did not receive a generic invocation", )); }; - let Value::Object(object) = this_value else { + let JsValue::Object(id) = this_value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, "not an object", )?)); }; - Ok(NativeConversion::Value(object)) + Ok(NativeConversion::Value(ObjectRef::from_borrowed_handle( + self.clone(), + *id, + )?)) } pub(crate) fn call_iterator_constructor( @@ -336,11 +339,13 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - constructor::finish( - self, - realm, - constructor::ConstructorStep::start(self, realm, &invocation)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + constructor::finish( + self, + realm, + constructor::ConstructorStep::start(self, realm, invocation)?, + ) + }) } pub(crate) fn call_iterator_constructor_accessor( @@ -350,11 +355,15 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - constructor::finish( - self, - realm, - constructor::ConstructorStep::accessor(self, realm, callable, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + constructor::finish( + self, + realm, + constructor::ConstructorStep::accessor( + self, realm, callable, invocation, arguments, + )?, + ) + }) } pub(crate) fn call_iterator_from( @@ -363,17 +372,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - from::finish( - self, - realm, - from::FromStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + from::finish( + self, + realm, + from::FromStep::start(self, realm, invocation, arguments)?, + ) + }) } - fn iterator_callable_value( + pub(crate) fn iterator_callable_value( &self, realm: ContextId, - value: Value, + value: &Value, ) -> Result, RuntimeError> { let Value::Object(object) = value else { return Ok(NativeConversion::Throw(self.new_native_error( @@ -382,7 +393,7 @@ impl Runtime { "not a function", )?)); }; - let Some(callable) = self.as_callable(&object)? else { + let Some(callable) = self.as_callable(object)? else { return Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, @@ -402,6 +413,13 @@ impl Runtime { let prototype = ObjectRef::from_borrowed_handle(self.clone(), prototype)?; let raw_source = self.raw_property_value(source)?; let raw_next = self.raw_property_value(next)?; + // The conversions allocated string/BigInt nodes with producer edges; + // the object retains its own copy edges, so the producer edges are + // released on every exit below. + let conversion_edges = [ + raw_source.conversion_node_edge(), + raw_next.conversion_node_edge(), + ]; let mut state = self.0.state.borrow_mut(); let shape = state.get_or_create_shape(Some(prototype.object_id()), &[])?; let retained_atoms = match state.retain_raw_value_atoms([&raw_source, &raw_next]) { @@ -409,6 +427,10 @@ impl Runtime { Err(error) => { let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + for edge in conversion_edges.into_iter().flatten() { + self.release_converted_node_edge(edge); + } return Err(error); } }; @@ -423,12 +445,19 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + for edge in conversion_edges.into_iter().flatten() { + self.release_converted_node_edge(edge); + } return Err(error.into()); } }; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; drop(state); + for edge in conversion_edges.into_iter().flatten() { + self.release_converted_node_edge(edge); + } Ok(ObjectRef::from_owned_handle(self.clone(), object)) } @@ -438,11 +467,13 @@ impl Runtime { kind: IteratorResumeKind, invocation: NativeInvocation, ) -> Result { - wrap::finish( - self, - realm, - wrap::WrapStep::start(self, realm, kind, &invocation)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + wrap::finish( + self, + realm, + wrap::WrapStep::start(self, realm, kind, invocation)?, + ) + }) } pub(crate) fn call_iterator_create_helper( @@ -452,11 +483,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - create::finish( - self, - realm, - create::CreateStep::start(self, realm, kind, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + create::finish( + self, + realm, + create::CreateStep::start(self, realm, kind, invocation, arguments)?, + ) + }) } fn new_iterator_helper( @@ -470,10 +503,19 @@ impl Runtime { ) -> Result { let prototype = self.iterator_realm_data(realm)?.helper_prototype; let prototype = ObjectRef::from_borrowed_handle(self.clone(), prototype)?; + let raw_next = self.raw_property_value(next)?; + let raw_callback = self.raw_property_value(callback)?; + // The conversions allocated string/BigInt nodes with producer edges; + // the object retains its own copy edges, so the producer edges are + // released on every exit below. + let conversion_edges = [ + raw_next.conversion_node_edge(), + raw_callback.conversion_node_edge(), + ]; let data = IteratorHelperData { source: source.object_id(), - next: self.raw_property_value(next)?, - callback: self.raw_property_value(callback)?, + next: raw_next, + callback: raw_callback, inner: None, count, kind, @@ -487,6 +529,10 @@ impl Runtime { Err(error) => { let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + for edge in conversion_edges.into_iter().flatten() { + self.release_converted_node_edge(edge); + } return Err(error); } }; @@ -516,17 +562,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - consume::finish( - self, - realm, - consume::ConsumeStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + consume::finish( self, realm, - consume::ConsumeKind::Predicate(kind), - &invocation, - arguments, - )?, - ) + consume::ConsumeStep::start( + self, + realm, + consume::ConsumeKind::Predicate(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_iterator_reduce( @@ -535,17 +583,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - consume::finish( - self, - realm, - consume::ConsumeStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + consume::finish( self, realm, - consume::ConsumeKind::Reduce, - &invocation, - arguments, - )?, - ) + consume::ConsumeStep::start( + self, + realm, + consume::ConsumeKind::Reduce, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_iterator_to_array( @@ -553,20 +603,22 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - consume::finish( - self, - realm, - consume::ConsumeStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + consume::finish( self, realm, - consume::ConsumeKind::Array, - &invocation, - &NativeArguments { - actual_arg_count: 0, - readable: Vec::new(), - }, - )?, - ) + consume::ConsumeStep::start( + self, + realm, + consume::ConsumeKind::Array, + invocation, + &NativeArguments { + actual_arg_count: 0, + readable: Vec::new(), + }, + )?, + ) + }) } pub(crate) fn call_iterator_helper_resume( @@ -575,11 +627,13 @@ impl Runtime { mode: IteratorResumeKind, invocation: NativeInvocation, ) -> Result { - helper::finish( - self, - realm, - helper::HelperResumeStep::start(self, realm, mode, &invocation)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + helper::finish( + self, + realm, + helper::HelperResumeStep::start(self, realm, mode, invocation)?, + ) + }) } fn set_helper_count(&self, helper: &ObjectRef, count: i64) -> Result<(), RuntimeError> { diff --git a/src/engine/builtins/iterator/step.rs b/src/engine/builtins/iterator/step.rs index f9c91b9d..65355937 100644 --- a/src/engine/builtins/iterator/step.rs +++ b/src/engine/builtins/iterator/step.rs @@ -4,7 +4,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey}, - value::Value, + value::{JsValue, Value}, vm::{Completion, call::NativeInvokeOutcome}, }; @@ -93,7 +93,7 @@ impl NextStep { }; let Some(callable) = callable else { return Ok(Self::Complete(ObjectIteratorStep::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not a function")?, ))); }; Ok(Self::call(realm, iterator, callable)) @@ -111,7 +111,7 @@ impl NextResume { runtime: &Runtime, result: NativeInvokeOutcome, ) -> Result { - match self.raw_completion(result)? { + match self.raw_completion(runtime, result)? { Ok(result) => Ok(NextStep::Complete(result)), Err(result) => self.resume(runtime, result), } @@ -121,6 +121,7 @@ impl NextResume { /// waiting NextStep. Only ordinary returned values need result parsing. pub(crate) fn raw_completion( &self, + runtime: &Runtime, result: NativeInvokeOutcome, ) -> Result, RuntimeError> { if !matches!(self.0.phase, NextPhase::Result) { @@ -130,6 +131,7 @@ impl NextResume { } Ok(match result { NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(if done { + runtime.release_jsvalue(value)?; ObjectIteratorStep::Done } else { ObjectIteratorStep::Yield(value) @@ -154,15 +156,17 @@ impl NextResume { let realm = self.0.realm; match self.0.phase { NextPhase::Result => { - let Value::Object(object) = value else { - return Ok(NextStep::Complete(ObjectIteratorStep::Throw( - runtime.new_native_error( - realm, - NativeErrorKind::Type, - "iterator must return an object", - )?, - ))); + let JsValue::Object(id) = &value else { + let error = runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "iterator must return an object", + )?; + runtime.release_jsvalue(value)?; + return Ok(NextStep::Complete(ObjectIteratorStep::Throw(error))); }; + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; + runtime.release_jsvalue(value)?; Ok(NextStep::Read { object: object.clone(), key: runtime @@ -174,7 +178,9 @@ impl NextResume { }) } NextPhase::Done(object) => { - if runtime.value_to_boolean(&value)? { + let done = runtime.value_to_boolean_jsvalue(&value)?; + runtime.release_jsvalue(value)?; + if done { return Ok(NextStep::Complete(ObjectIteratorStep::Done)); } Ok(NextStep::Read { @@ -292,38 +298,42 @@ impl CloseResume { let value = match reply { Completion::Return(value) => value, Completion::Throw(value) => { - return Ok(CloseStep::Complete(if preserving { - self.0.completion - } else { - Completion::Throw(value) - })); + if preserving { + runtime.release_jsvalue(value)?; + return Ok(CloseStep::Complete(self.0.completion)); + } + return Ok(CloseStep::Complete(Completion::Throw(value))); } }; if self.0.called { - return Ok(CloseStep::Complete( - if preserving || matches!(value, Value::Object(_)) { - self.0.completion - } else { - Completion::Throw(runtime.new_native_error( - self.0.realm, - NativeErrorKind::Type, - "not an object", - )?) - }, - )); + let valid = preserving || matches!(value, JsValue::Object(_)); + runtime.release_jsvalue(value)?; + return Ok(CloseStep::Complete(if valid { + self.0.completion + } else { + Completion::Throw(runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "not an object", + )?) + })); } - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { return Ok(CloseStep::Complete(self.0.completion)); } - let callable = match value { - Value::Object(ref object) => runtime.as_callable(object)?, + let callable = match &value { + JsValue::Object(id) => { + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; + runtime.as_callable(&object)? + } _ => None, }; + runtime.release_jsvalue(value)?; let Some(callable) = callable else { return Ok(CloseStep::Complete(if preserving { self.0.completion } else { - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not a function", @@ -388,10 +398,13 @@ mod raw_completion_tests { let id = object.object_id(); assert!(matches!( resume - .raw_completion(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Object(object), - done: true, - }) + .raw_completion( + &runtime, + NativeInvokeOutcome::IteratorNextRaw { + value: runtime.unroot_value(&Value::Object(object)).unwrap(), + done: true, + } + ) .unwrap(), Ok(ObjectIteratorStep::Done) )); @@ -403,13 +416,13 @@ mod raw_completion_tests { })); for reply in [ NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }, - NativeInvokeOutcome::Completion(Completion::Throw(Value::Int(7))), + NativeInvokeOutcome::Completion(Completion::Throw(JsValue::Int(7))), ] { assert!(matches!( - wrong.raw_completion(reply), + wrong.raw_completion(&runtime, reply), Err(RuntimeError::Invariant( "raw iterator reply has the wrong phase" )) @@ -432,7 +445,7 @@ mod raw_completion_tests { .raw( &runtime, NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: false, } ) @@ -451,25 +464,32 @@ mod raw_completion_tests { phase: NextPhase::Result, })); let marker = Value::Object(runtime.new_object(None).unwrap()); + let marker_internal = runtime.unroot_value(&marker).unwrap(); let Ok(ObjectIteratorStep::Yield(value)) = resume - .raw_completion(NativeInvokeOutcome::IteratorNextRaw { - value: marker.clone(), - done: false, - }) + .raw_completion( + &runtime, + NativeInvokeOutcome::IteratorNextRaw { + value: runtime.dup_jsvalue(&marker_internal).unwrap(), + done: false, + }, + ) .unwrap() else { panic!("yield lost"); }; - assert_eq!(value, marker); + assert_eq!(runtime.root_value(&value).unwrap(), marker); let Ok(ObjectIteratorStep::Throw(value)) = resume - .raw_completion(NativeInvokeOutcome::Completion(Completion::Throw( - marker.clone(), - ))) + .raw_completion( + &runtime, + NativeInvokeOutcome::Completion(Completion::Throw( + runtime.dup_jsvalue(&marker_internal).unwrap(), + )), + ) .unwrap() else { panic!("throw lost"); }; - assert_eq!(value, marker); + assert_eq!(runtime.root_value(&value).unwrap(), marker); } #[test] @@ -483,11 +503,13 @@ mod raw_completion_tests { realm: context.realm, phase: NextPhase::Result, })); - let reply = NativeInvokeOutcome::Completion(Completion::Return(result)); + let reply = NativeInvokeOutcome::Completion(Completion::Return( + runtime.unroot_value(&result).unwrap(), + )); let step = if wrapper { resume.raw(&runtime, reply).unwrap() } else { - let Err(result) = resume.raw_completion(reply).unwrap() else { + let Err(result) = resume.raw_completion(&runtime, reply).unwrap() else { panic!("ordinary result skipped parsing"); }; assert_eq!( @@ -501,7 +523,7 @@ mod raw_completion_tests { else { panic!("result lost"); }; - assert_eq!(value, marker); + assert_eq!(runtime.root_value(&value).unwrap(), marker); assert_eq!( context.eval("trace").unwrap(), Value::String(crate::engine::value::JsString::from_static("dv")) diff --git a/src/engine/builtins/iterator/wrap.rs b/src/engine/builtins/iterator/wrap.rs index 7f28cf53..5365ed33 100644 --- a/src/engine/builtins/iterator/wrap.rs +++ b/src/engine/builtins/iterator/wrap.rs @@ -7,7 +7,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, HeapError, IteratorResumeKind}, object::{CallableRef, ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeInvocation}, }; pub(crate) enum WrapStep { @@ -49,9 +49,13 @@ impl WrapStep { mode: IteratorResumeKind, invocation: &NativeInvocation, ) -> Result { - let receiver = match runtime.iterator_receiver(realm, invocation.clone())? { + let receiver = match runtime.iterator_receiver(realm, invocation)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let state = { runtime @@ -65,7 +69,7 @@ impl WrapStep { Ok(state) => state, Err(HeapError::Invariant(_)) => { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an Iterator Wrap", @@ -83,7 +87,7 @@ impl WrapStep { })); match mode { IteratorResumeKind::Return => Ok({ - let __pending_field_receiver = source; + let __pending_field_receiver = runtime.into_jsvalue(source)?; let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Return)?; let __pending_field_resume = resume; @@ -98,7 +102,7 @@ impl WrapStep { if let Value::Object(iterator) = source { return Ok({ let __pending_field_iterator = iterator; - let __pending_field_method = method; + let __pending_field_method = runtime.into_jsvalue(method)?; let __pending_field_resume = { let updated_0 = Phase::Next; let mut resident = resume; @@ -112,15 +116,17 @@ impl WrapStep { ) }); } - let callable = match runtime.iterator_callable_value(realm, method)? { + let callable = match runtime.iterator_callable_value(realm, &method)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = source; + let __pending_field_receiver = runtime.into_jsvalue(source)?; let __pending_field_resume = { let updated_0 = Phase::NextResult; let mut resident = resume; @@ -152,26 +158,34 @@ impl WrapResume { }); } let value = match reply { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(WrapStep::Complete(Completion::Throw(value))), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(WrapStep::Complete(Completion::Throw(value))); + } }; match self.0.phase { Phase::ReturnMethod => { if matches!(value, Value::Undefined | Value::Null) { - return Ok(WrapStep::Complete(Completion::Return(Value::Object( - runtime.new_iterator_result(self.0.realm, Value::Undefined, true)?, - )))); + return Ok(WrapStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(runtime.new_iterator_result( + self.0.realm, + Value::Undefined, + true, + )?))?, + ))); } - let callable = match runtime.iterator_callable_value(self.0.realm, value)? { + let callable = match runtime.iterator_callable_value(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(WrapStep::Complete(Completion::Throw(value))); + return Ok(WrapStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.phase = Phase::ReturnResult; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = self.0.source.clone(); + let __pending_field_receiver = runtime.into_jsvalue(self.0.source.clone())?; let __pending_field_resume = self; WrapStep::request_call( __pending_field_callable, @@ -181,9 +195,9 @@ impl WrapResume { }) } Phase::ReturnResult => Ok(WrapStep::Complete(if matches!(value, Value::Object(_)) { - Completion::Return(value) + Completion::Return(runtime.into_jsvalue(value)?) } else { - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "iterator must return an object", @@ -206,12 +220,16 @@ impl WrapResume { ObjectIteratorStep::Throw(value) => { return Ok(WrapStep::Complete(Completion::Throw(value))); } - ObjectIteratorStep::Yield(value) => (value, false), + ObjectIteratorStep::Yield(value) => (runtime.root_and_release_jsvalue(value)?, false), ObjectIteratorStep::Done => (Value::Undefined, true), }; - Ok(WrapStep::Complete(Completion::Return(Value::Object( - runtime.new_iterator_result(self.0.realm, value, done)?, - )))) + Ok(WrapStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(runtime.new_iterator_result( + self.0.realm, + value, + done, + )?))?, + ))) } } pub(crate) fn finish( @@ -223,7 +241,7 @@ pub(crate) fn finish( step = match step { WrapStep::Complete(result) => return Ok(result), WrapStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -232,7 +250,7 @@ pub(crate) fn finish( } WrapStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &[])?, @@ -240,7 +258,7 @@ pub(crate) fn finish( } WrapStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let method = resume.take_next_method(); + let method = runtime.root_and_release_jsvalue(resume.take_next_method())?; resume.next( runtime, finish_next( @@ -267,30 +285,38 @@ pub(crate) fn finish( #[derive(Default)] struct WrapStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, + call_receiver: Option, next_iterator: Option, - next_method: Option, + next_method: Option, parse_result: Option, } impl WrapStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: WrapResume) -> Self { + pub(crate) fn request_read( + receiver: JsValue, + key: PropertyKey, + mut resume: WrapResume, + ) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, + receiver: JsValue, mut resume: WrapResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); resume.0.pending_effect.call_receiver = Some(receiver); Self::Call { resume } } - pub(crate) fn request_next(iterator: ObjectRef, method: Value, mut resume: WrapResume) -> Self { + pub(crate) fn request_next( + iterator: ObjectRef, + method: JsValue, + mut resume: WrapResume, + ) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); resume.0.pending_effect.next_method = Some(method); Self::Next { resume } @@ -301,7 +327,7 @@ impl WrapStep { } } impl WrapResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -322,7 +348,7 @@ impl WrapResume { .take() .expect("WrapStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver @@ -336,7 +362,7 @@ impl WrapResume { .take() .expect("WrapStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.0 .pending_effect .next_method diff --git a/src/engine/builtins/json/mod.rs b/src/engine/builtins/json/mod.rs index 4fc36959..df8ec35c 100644 --- a/src/engine/builtins/json/mod.rs +++ b/src/engine/builtins/json/mod.rs @@ -96,11 +96,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "JSON method did not receive a generic invocation", )); }; + invocation.release(self)?; match kind { JsonNativeKind::IsRawJson => self.call_json_is_raw_json(arguments), JsonNativeKind::Parse => self.call_json_parse(realm, arguments), diff --git a/src/engine/builtins/json/raw.rs b/src/engine/builtins/json/raw.rs index 9484438d..97cd47ee 100644 --- a/src/engine/builtins/json/raw.rs +++ b/src/engine/builtins/json/raw.rs @@ -6,8 +6,8 @@ use crate::engine::api::runtime_error::RuntimeError; use crate::engine::heap::{ContextId, ObjectData, ObjectPayload}; use crate::engine::object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::NativeArguments; @@ -17,17 +17,20 @@ impl Runtime { arguments: &NativeArguments, ) -> Result { let branded = match &arguments.readable[0] { - Value::Object(object) => self.is_raw_json_object(object)?, - Value::Undefined - | Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_) - | Value::BigInt(_) - | Value::String(_) - | Value::Symbol(_) => false, + JsValue::Object(id) => { + let object = ObjectRef::from_borrowed_handle(self.clone(), *id)?; + self.is_raw_json_object(&object)? + } + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) + | JsValue::BigInt(_) + | JsValue::String(_) + | JsValue::Symbol(_) => false, }; - Ok(Completion::Return(Value::Bool(branded))) + Ok(Completion::Return(JsValue::Bool(branded))) } pub(crate) fn call_json_raw_json( @@ -35,10 +38,8 @@ impl Runtime { realm: ContextId, arguments: &NativeArguments, ) -> Result { - RawResume { realm }.string( - self, - self.native_to_js_string(realm, &arguments.readable[0])?, - ) + let argument = self.root_value(&arguments.readable[0])?; + RawResume { realm }.string(self, self.native_to_js_string(realm, &argument)?) } fn raw_json_from_string( @@ -81,7 +82,9 @@ impl Runtime { )); } self.prevent_extensions(&object)?; - Ok(Completion::Return(Value::Object(object))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(object))?, + )) } pub(crate) fn is_raw_json_object(&self, object: &ObjectRef) -> Result { @@ -120,7 +123,7 @@ impl Runtime { } fn invalid_raw_json(&self, realm: ContextId) -> Result { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Syntax, "invalid rawJSON string", @@ -146,7 +149,7 @@ impl RawResume { ) -> Result { match reply { NativeConversion::Value(source) => runtime.raw_json_from_string(self.realm, source), - NativeConversion::Throw(value) => Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => Ok(Completion::Throw(runtime.into_jsvalue(value)?)), } } } diff --git a/src/engine/builtins/json/reviver.rs b/src/engine/builtins/json/reviver.rs index cb62cdf1..cfe1917a 100644 --- a/src/engine/builtins/json/reviver.rs +++ b/src/engine/builtins/json/reviver.rs @@ -13,7 +13,7 @@ use crate::engine::object::{ CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::NativeArguments; @@ -82,10 +82,32 @@ impl std::ops::DerefMut for ParseResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct ParseResumeState { + runtime: Runtime, pending_effect: ParseStepPending, state: State, phase: Phase, } +impl Drop for ParseResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.string_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.number_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} enum Phase { Source(Value), Read, @@ -122,13 +144,16 @@ enum Children { } impl ParseStep { pub(crate) fn start( - _runtime: &Runtime, + runtime: &Runtime, realm: ContextId, arguments: &NativeArguments, ) -> Result { - Ok(Self::request_string(arguments.readable[0].clone(), { - let phase = Phase::Source(arguments.readable[1].clone()); + let source = runtime.dup_jsvalue(&arguments.readable[0])?; + let reviver = runtime.root_value(&arguments.readable[1])?; + Ok(Self::request_string(source, { + let phase = Phase::Source(reviver); let mut owner = Box::new(ParseResumeState { + runtime: runtime.clone(), pending_effect: Default::default(), phase: Phase::Read, state: State { @@ -159,7 +184,7 @@ impl ParseResumeState { ) -> Result { if self.frames.len() > MAX_JSON_REVIVER_DEPTH { return Ok(ParseStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.realm, NativeErrorKind::Internal, "stack overflow", @@ -168,7 +193,11 @@ impl ParseResumeState { } if self.frames.try_reserve(1).is_err() { return Ok(ParseStep::Complete(Completion::Throw( - runtime.new_native_error(self.realm, NativeErrorKind::Internal, "out of memory")?, + runtime.new_native_error_jsvalue( + self.realm, + NativeErrorKind::Internal, + "out of memory", + )?, ))); } self.frames.push(Node { @@ -218,15 +247,16 @@ impl ParseResumeState { let object = object.clone(); return self.enter(runtime, object, key, record); } - let receiver = Value::Object(node.holder.clone()); - let name = Value::String( - runtime - .0 - .state - .borrow() - .atoms - .to_js_string(node.key.atom())?, - ); + let receiver = runtime.into_jsvalue(Value::Object(node.holder.clone()))?; + // End the state borrow before the conversion: `into_jsvalue` allocates + // a string node and must re-borrow the runtime state. + let name = runtime + .0 + .state + .borrow() + .atoms + .to_js_string(node.key.atom())?; + let name = runtime.into_jsvalue(Value::String(name))?; let context = node .context .clone() @@ -234,12 +264,16 @@ impl ParseResumeState { let mut arguments = Vec::new(); if arguments.try_reserve_exact(3).is_err() { return Ok(ParseStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Internal, "out of memory")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Internal, + "out of memory", + )?, ))); } arguments.push(name); - arguments.push(node.value.clone()); - arguments.push(Value::Object(context)); + arguments.push(runtime.unroot_value(&node.value)?); + arguments.push(runtime.into_jsvalue(Value::Object(context))?); let callable = self .reviver .clone() @@ -301,7 +335,9 @@ impl ParseResume { let source = match reply { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Source(reviver) = std::mem::replace(&mut self.0.phase, Phase::Read) else { @@ -325,11 +361,15 @@ impl ParseResume { match runtime.parse_json_text(state.realm, &state.source, state.reviver.is_some())? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Some(root) = root else { - return Ok(ParseStep::Complete(Completion::Return(parsed))); + return Ok(ParseStep::Complete(Completion::Return( + runtime.into_jsvalue(parsed)?, + ))); }; let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Literal0)?; match runtime.define_json_reviver_property(state.realm, &root, &key, parsed)? { @@ -340,7 +380,9 @@ impl ParseResume { )); } PropertyDefineOutcome::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } let record = record.map(Rc::new); @@ -355,7 +397,7 @@ impl ParseResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(ParseStep::Complete(Completion::Throw(value))), }; match std::mem::replace(&mut self.0.phase, Phase::Read) { @@ -370,7 +412,9 @@ impl ParseResume { match runtime.internal_is_array(realm, &Value::Object(object.clone()))? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if array { @@ -412,14 +456,16 @@ impl ParseResume { )); } PropertyDefineOutcome::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } self.0.next(runtime) } } - Phase::Length => Ok(ParseStep::request_number(value, { + Phase::Length => Ok(ParseStep::request_number(runtime.into_jsvalue(value)?, { let phase = Phase::Number; let mut owner = self.0; owner.phase = phase; @@ -432,7 +478,9 @@ impl ParseResume { .pop() .ok_or(RuntimeError::Invariant("JSON reviver reply lost node"))?; if self.0.frames.is_empty() { - return Ok(ParseStep::Complete(Completion::Return(value))); + return Ok(ParseStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))); } let resume = { let phase = Phase::Applied; @@ -475,7 +523,9 @@ impl ParseResume { let number = match reply { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.top()?.children = Children::Array { @@ -498,7 +548,9 @@ impl ParseResume { NativeConversion::Value(keys) => { self.0.enumerate(runtime, keys.into_iter(), Vec::new()) } - NativeConversion::Throw(value) => Ok(ParseStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), } } pub(crate) fn boolean( @@ -509,7 +561,9 @@ impl ParseResume { let value = match reply { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ParseStep::Complete(Completion::Throw(value))); + return Ok(ParseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match std::mem::replace(&mut self.0.phase, Phase::Read) { @@ -521,7 +575,7 @@ impl ParseResume { if value { if selected.try_reserve(1).is_err() { return Ok(ParseStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Internal, "out of memory", @@ -549,11 +603,18 @@ fn finish( ParseStep::Complete(result) => return Ok(result), ParseStep::String { mut resume } => { let value = resume.take_string_value(); - resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? + resume.string( + runtime, + runtime + .native_to_js_string(realm, &runtime.root_and_release_jsvalue(value)?)?, + )? } ParseStep::Number { mut resume } => { let value = resume.take_number_value(); - resume.number(runtime, runtime.native_to_number(realm, &value)?)? + resume.number( + runtime, + runtime.native_to_number(realm, &runtime.root_and_release_jsvalue(value)?)?, + )? } ParseStep::Read { mut resume } => { let object = resume.take_read_object(); @@ -577,8 +638,12 @@ fn finish( } ParseStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -677,8 +742,10 @@ mod ownership_tests { let arguments = NativeArguments { actual_arg_count: 2, readable: vec![ - Value::String(JsString::from_static("{\"a\":{},\"b\":{}}")), - callback, + runtime + .into_jsvalue(Value::String(JsString::from_static("{\"a\":{},\"b\":{}}"))) + .unwrap(), + runtime.into_jsvalue(callback).unwrap(), ], }; let ParseStep::String { mut resume } = @@ -686,11 +753,16 @@ mod ownership_tests { else { panic!("expected source conversion"); }; - let Value::String(source) = resume.take_string_value() else { + let Value::String(source) = runtime + .root_and_release_jsvalue(resume.take_string_value()) + .unwrap() + else { panic!("expected payload"); }; let resident_owner = (&*resume.0) as *const ParseResumeState; - drop(arguments); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } let step = until_call( &runtime, context.realm, @@ -706,29 +778,46 @@ mod ownership_tests { let receiver = resume.take_call_receiver(); assert_eq!(resident_owner, (&*resume.0) as *const ParseResumeState); drop(callable); - drop(receiver); - assert_eq!(arguments[0].to_js_string().unwrap().to_utf8_lossy(), "a"); - let Value::Object(first) = &arguments[1] else { + runtime.release_jsvalue(receiver).unwrap(); + assert_eq!( + runtime + .root_value(&arguments[0]) + .unwrap() + .to_js_string() + .unwrap() + .to_utf8_lossy(), + "a" + ); + let JsValue::Object(first_id) = &arguments[1] else { panic!("expected first child"); }; - let first_id = first.object_id(); - drop(arguments); + let first_id = *first_id; + for value in arguments { + runtime.release_jsvalue(value).unwrap(); + } let step = until_call( &runtime, context.realm, resume - .resume(&runtime, Completion::Return(Value::Undefined)) + .resume(&runtime, Completion::Return(JsValue::Undefined)) .unwrap(), ); let ParseStep::Call { resume } = &step else { panic!("expected b callback"); }; let arguments = resume.0.pending_effect.call_arguments.as_ref().unwrap(); - assert_eq!(arguments[0].to_js_string().unwrap().to_utf8_lossy(), "b"); - let Value::Object(second) = &arguments[1] else { + assert_eq!( + runtime + .root_value(&arguments[0]) + .unwrap() + .to_js_string() + .unwrap() + .to_utf8_lossy(), + "b" + ); + let JsValue::Object(second_id) = arguments[1] else { panic!("expected second child"); }; - let second_id = second.object_id(); let context_id = resume .state .frames @@ -748,7 +837,7 @@ mod ownership_tests { runtime .get_property_in_realm(context.realm, root, &key) .unwrap(), - Completion::Return(Value::Undefined) + Completion::Return(JsValue::Undefined) )); drop(key); runtime.run_gc().unwrap(); @@ -761,14 +850,20 @@ mod ownership_tests { callback_id, ]; for id in ids { - assert!(runtime.0.state.borrow().heap.object(id).is_ok()); + assert!( + runtime.0.state.borrow().heap.object(id).is_ok(), + "pre-abandonment missing {id:?}" + ); } drop(step); runtime.run_gc().unwrap(); - for id in ids { + for (label, id) in ["first", "second", "root", "holder", "context", "callback"] + .into_iter() + .zip(ids) + { assert!( runtime.0.state.borrow().heap.object(id).is_err(), - "abandoned reviver retained {id:?}" + "abandoned reviver retained {label}" ); } drop(context); @@ -779,16 +874,16 @@ mod ownership_tests { #[derive(Default)] struct ParseStepPending { - string_value: Option, + string_value: Option, read_object: Option, read_key: Option, - number_value: Option, + number_value: Option, keys_object: Option, enumerable_object: Option, enumerable_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, delete_object: Option, delete_key: Option, define_object: Option, @@ -796,7 +891,7 @@ struct ParseStepPending { define_descriptor: Option, } impl ParseStep { - pub(crate) fn request_string(value: Value, mut resume: ParseResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: ParseResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } @@ -809,7 +904,7 @@ impl ParseStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: ParseResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: ParseResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -828,8 +923,8 @@ impl ParseStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ParseResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -859,7 +954,7 @@ impl ParseStep { } } impl ParseResume { - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value @@ -880,7 +975,7 @@ impl ParseResume { .take() .expect("ParseStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -915,14 +1010,14 @@ impl ParseResume { .take() .expect("ParseStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ParseStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/json/stringify/operation.rs b/src/engine/builtins/json/stringify/operation.rs index 07bd9ec2..cb0c6946 100644 --- a/src/engine/builtins/json/stringify/operation.rs +++ b/src/engine/builtins/json/stringify/operation.rs @@ -9,7 +9,7 @@ use crate::engine::{ atom::PropertyKeyKind, heap::ContextId, object::{CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey}, - value::{JsString, JsStringBuilder, Value, conversion::NativeConversion}, + value::{JsString, JsStringBuilder, JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeArguments}, }; @@ -36,10 +36,35 @@ impl std::ops::DerefMut for StringifyResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct StringifyResumeState { + runtime: Runtime, pending_effect: StringifyStepPending, state: State, phase: Phase, } +impl Drop for StringifyResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.string_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.number_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} struct List { object: ObjectRef, index: u64, @@ -90,12 +115,15 @@ enum Phase { key: PropertyKey, }, } -fn result(value: JsonStringifyResult) -> Result { +fn result( + runtime: &Runtime, + value: JsonStringifyResult, +) -> Result { match value { Ok(step) => Ok(step), - Err(JsonStringifyFailure::Throw(value)) => { - Ok(StringifyStep::Complete(Completion::Throw(value))) - } + Err(JsonStringifyFailure::Throw(value)) => Ok(StringifyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), Err(JsonStringifyFailure::Runtime(error)) => Err(error), } } @@ -105,10 +133,12 @@ fn converted(reply: NativeConversion) -> JsonStringifyResult { NativeConversion::Throw(value) => Err(JsonStringifyFailure::Throw(value)), } } -fn returned(reply: Completion) -> JsonStringifyResult { +fn returned(runtime: &Runtime, reply: Completion) -> JsonStringifyResult { match reply { - Completion::Return(value) => Ok(value), - Completion::Throw(value) => Err(JsonStringifyFailure::Throw(value)), + Completion::Return(value) => Ok(runtime.root_and_release_jsvalue(value)?), + Completion::Throw(value) => Err(JsonStringifyFailure::Throw( + runtime.root_and_release_jsvalue(value)?, + )), } } impl StringifyStep { @@ -117,42 +147,46 @@ impl StringifyStep { realm: ContextId, arguments: &NativeArguments, ) -> Result { - result((|| { - runtime.0.state.borrow().heap.context(realm)?; - let replacer_value = &arguments.readable[1]; - let replacer = match replacer_value { - Value::Object(object) => runtime.as_callable(object)?, - _ => None, - }; - let state = Box::new(StringifyResumeState { - pending_effect: Default::default(), - phase: Phase::GapString, - state: State { - realm, - replacer, - property_list: None, - gap: JsString::from_static(""), - to_json_key: None, - stack: Vec::new(), - output: JsStringBuilder::new(0), - tasks: Vec::new(), - root: arguments.readable[0].clone(), - space: arguments.readable[2].clone(), - }, - }); - if state.replacer.is_none() - && let Value::Object(object) = replacer_value - && converted(runtime.internal_is_array(realm, replacer_value)?)? - { - return state.read( - runtime, - object.clone(), - "length", - Phase::ListLength(object.clone()), - ); - } - state.gap(runtime) - })()) + result( + runtime, + (|| { + runtime.0.state.borrow().heap.context(realm)?; + let replacer_value = runtime.root_value(&arguments.readable[1])?; + let replacer = match &replacer_value { + Value::Object(object) => runtime.as_callable(object)?, + _ => None, + }; + let state = Box::new(StringifyResumeState { + runtime: runtime.clone(), + pending_effect: Default::default(), + phase: Phase::GapString, + state: State { + realm, + replacer, + property_list: None, + gap: JsString::from_static(""), + to_json_key: None, + stack: Vec::new(), + output: JsStringBuilder::new(0), + tasks: Vec::new(), + root: runtime.root_value(&arguments.readable[0])?, + space: runtime.root_value(&arguments.readable[2])?, + }, + }); + if state.replacer.is_none() + && let Value::Object(object) = &replacer_value + && converted(runtime.internal_is_array(realm, &replacer_value)?)? + { + return state.read( + runtime, + object.clone(), + "length", + Phase::ListLength(object.clone()), + ); + } + state.gap(runtime) + })(), + ) } } impl StringifyResumeState { @@ -164,7 +198,7 @@ impl StringifyResumeState { phase: Phase, ) -> JsonStringifyResult { Ok(StringifyStep::request_read( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, runtime.intern_property_key(key)?, { let phase = phase; @@ -185,7 +219,7 @@ impl StringifyResumeState { } let key = runtime.intern_property_key(&list.index.to_string())?; Ok(StringifyStep::request_read( - Value::Object(list.object.clone()), + runtime.into_jsvalue(Value::Object(list.object.clone()))?, key, { let phase = Phase::ListItem(list); @@ -199,20 +233,26 @@ impl StringifyResumeState { match &self.space { Value::Object(object) => match runtime.json_wrapper_kind(object)? { JsonWrapperKind::String => { - return Ok(StringifyStep::request_string(self.space.clone(), { - let phase = Phase::GapString; - let mut owner = self; - owner.phase = phase; - StringifyResume(owner) - })); + return Ok(StringifyStep::request_string( + runtime.unroot_value(&self.space)?, + { + let phase = Phase::GapString; + let mut owner = self; + owner.phase = phase; + StringifyResume(owner) + }, + )); } JsonWrapperKind::Number => { - return Ok(StringifyStep::request_number(self.space.clone(), { - let phase = Phase::GapNumber; - let mut owner = self; - owner.phase = phase; - StringifyResume(owner) - })); + return Ok(StringifyStep::request_number( + runtime.unroot_value(&self.space)?, + { + let phase = Phase::GapNumber; + let mut owner = self; + owner.phase = phase; + StringifyResume(owner) + }, + )); } _ => {} }, @@ -291,7 +331,7 @@ impl StringifyResumeState { ) -> JsonStringifyResult { if matches!(check.value, Value::Object(_) | Value::BigInt(_)) { Ok(StringifyStep::request_read( - check.value.clone(), + runtime.unroot_value(&check.value)?, self.to_json_key .clone() .ok_or(RuntimeError::Invariant("JSON stringify lost toJSON key"))?, @@ -320,11 +360,11 @@ impl StringifyResumeState { "out of memory", )?)); } - arguments.push(Value::String(check.key.clone())); - arguments.push(check.value.clone()); + arguments.push(runtime.into_jsvalue(Value::String(check.key.clone()))?); + arguments.push(runtime.unroot_value(&check.value)?); return Ok(StringifyStep::request_call( callable.clone(), - Value::Object(check.holder.clone()), + runtime.into_jsvalue(Value::Object(check.holder.clone()))?, arguments, { let phase = Phase::Replacer(check); @@ -350,7 +390,7 @@ impl StringifyResumeState { Destination::Root => { if !accepted { return Ok(StringifyStep::Complete(Completion::Return( - Value::Undefined, + runtime.into_jsvalue(Value::Undefined)?, ))); } self.tasks.push(Task::Value { @@ -451,7 +491,7 @@ impl StringifyResumeState { match runtime.json_wrapper_kind(&object)? { JsonWrapperKind::String => { return Ok(StringifyStep::request_string( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, { let phase = Phase::WrapperString; let mut owner = self; @@ -462,7 +502,7 @@ impl StringifyResumeState { } JsonWrapperKind::Number => { return Ok(StringifyStep::request_number( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, { let phase = Phase::WrapperNumber(indent); let mut owner = self; @@ -511,7 +551,7 @@ impl StringifyResumeState { next_indent, }); return Ok(StringifyStep::request_read( - Value::Object(array.clone()), + runtime.into_jsvalue(Value::Object(array.clone()))?, key, { let phase = Phase::ReadCheck { @@ -552,7 +592,7 @@ impl StringifyResumeState { next_indent, }); return Ok(StringifyStep::request_read( - Value::Object(object.clone()), + runtime.into_jsvalue(Value::Object(object.clone()))?, key, { let phase = Phase::ReadCheck { @@ -568,9 +608,10 @@ impl StringifyResumeState { } } } - Ok(StringifyStep::Complete(Completion::Return(Value::String( - self.state.output.finish()?, - )))) + let output = std::mem::replace(&mut self.state.output, JsStringBuilder::new(0)); + Ok(StringifyStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::String(output.finish()?))?, + ))) } fn begin( mut self: Box, @@ -666,19 +707,22 @@ impl StringifyResume { runtime: &Runtime, reply: Completion, ) -> Result { - result(self.value(runtime, reply)) + result(runtime, self.value(runtime, reply)) } fn value(mut self, runtime: &Runtime, reply: Completion) -> JsonStringifyResult { - let value = returned(reply)?; + let value = returned(runtime, reply)?; let phase = std::mem::replace(&mut self.0.phase, Phase::GapString); let state = self.0; match phase { - Phase::ListLength(object) => Ok(StringifyStep::request_number(value, { - let phase = Phase::ListNumber(object); - let mut owner = state; - owner.phase = phase; - StringifyResume(owner) - })), + Phase::ListLength(object) => Ok(StringifyStep::request_number( + runtime.into_jsvalue(value)?, + { + let phase = Phase::ListNumber(object); + let mut owner = state; + owner.phase = phase; + StringifyResume(owner) + }, + )), Phase::ListItem(mut list) => { let string = match &value { Value::String(_) | Value::Int(_) | Value::Float(_) => true, @@ -689,12 +733,15 @@ impl StringifyResume { _ => false, }; if string { - Ok(StringifyStep::request_string(value, { - let phase = Phase::ListString(list); - let mut owner = state; - owner.phase = phase; - StringifyResume(owner) - })) + Ok(StringifyStep::request_string( + runtime.into_jsvalue(value)?, + { + let phase = Phase::ListString(list); + let mut owner = state; + owner.phase = phase; + StringifyResume(owner) + }, + )) } else { list.index += 1; state.list(runtime, list) @@ -725,10 +772,10 @@ impl StringifyResume { "out of memory", )?)); } - arguments.push(Value::String(check.key.clone())); + arguments.push(runtime.into_jsvalue(Value::String(check.key.clone()))?); Ok(StringifyStep::request_call( callable, - check.value.clone(), + runtime.unroot_value(&check.value)?, arguments, { let phase = Phase::ToJsonCall(check); @@ -760,12 +807,15 @@ impl StringifyResume { state.output.push_js_string(&source)?; state.advance(runtime) } - Phase::ArrayLength(start) => Ok(StringifyStep::request_number(value, { - let phase = Phase::ArrayNumber(start); - let mut owner = state; - owner.phase = phase; - StringifyResume(owner) - })), + Phase::ArrayLength(start) => Ok(StringifyStep::request_number( + runtime.into_jsvalue(value)?, + { + let phase = Phase::ArrayNumber(start); + let mut owner = state; + owner.phase = phase; + StringifyResume(owner) + }, + )), _ => Err(RuntimeError::Invariant("JSON stringify unexpected value reply").into()), } } @@ -774,109 +824,128 @@ impl StringifyResume { runtime: &Runtime, reply: NativeConversion, ) -> Result { - result((|| { - let value = converted(reply)?; - match std::mem::replace(&mut self.0.phase, Phase::GapString) { - Phase::ListString(mut list) => { - if !list.items.iter().any(|item| item == &value) { - list.items.push(value); + result( + runtime, + (|| { + let value = converted(reply)?; + match std::mem::replace(&mut self.0.phase, Phase::GapString) { + Phase::ListString(mut list) => { + if !list.items.iter().any(|item| item == &value) { + list.items.push(value); + } + list.index += 1; + self.0.list(runtime, list) } - list.index += 1; - self.0.list(runtime, list) - } - Phase::GapString => { - let gap = value.sub_string(0, value.len().min(10)); - self.0.root(runtime, gap) - } - Phase::WrapperString => { - let mut state = self.0; - state.append_quoted(&value)?; - state.advance(runtime) + Phase::GapString => { + let gap = value.sub_string(0, value.len().min(10)); + self.0.root(runtime, gap) + } + Phase::WrapperString => { + let mut state = self.0; + state.append_quoted(&value)?; + state.advance(runtime) + } + _ => Err( + RuntimeError::Invariant("JSON stringify unexpected string reply").into(), + ), } - _ => Err(RuntimeError::Invariant("JSON stringify unexpected string reply").into()), - } - })()) + })(), + ) } pub(crate) fn number( mut self, runtime: &Runtime, reply: NativeConversion, ) -> Result { - result((|| { - let value = converted(reply)?; - let phase = std::mem::replace(&mut self.0.phase, Phase::GapString); - let mut state = self.0; - match phase { - Phase::ListNumber(object) => state.list( - runtime, - List { - object, - index: 0, - length: Runtime::length_from_number(value), - items: Vec::new(), - }, - ), - Phase::GapNumber => state.number_gap(runtime, value), - Phase::WrapperNumber(indent) => { - state.tasks.push(Task::Value { - value: Value::number(value), - indent, - }); - state.advance(runtime) - } - Phase::ArrayNumber(start) => { - state.output.push_utf8("[")?; - state.tasks.push(Task::ArrayElement { - array: start.object, - index: 0, - length: Runtime::length_from_number(value), - indent: start.indent, - next_indent: start.next_indent, - }); - state.advance(runtime) + result( + runtime, + (|| { + let value = converted(reply)?; + let phase = std::mem::replace(&mut self.0.phase, Phase::GapString); + let mut state = self.0; + match phase { + Phase::ListNumber(object) => state.list( + runtime, + List { + object, + index: 0, + length: Runtime::length_from_number(value), + items: Vec::new(), + }, + ), + Phase::GapNumber => state.number_gap(runtime, value), + Phase::WrapperNumber(indent) => { + state.tasks.push(Task::Value { + value: Value::number(value), + indent, + }); + state.advance(runtime) + } + Phase::ArrayNumber(start) => { + state.output.push_utf8("[")?; + state.tasks.push(Task::ArrayElement { + array: start.object, + index: 0, + length: Runtime::length_from_number(value), + indent: start.indent, + next_indent: start.next_indent, + }); + state.advance(runtime) + } + _ => Err( + RuntimeError::Invariant("JSON stringify unexpected number reply").into(), + ), } - _ => Err(RuntimeError::Invariant("JSON stringify unexpected number reply").into()), - } - })()) + })(), + ) } pub(crate) fn keys( mut self, runtime: &Runtime, reply: NativeConversion>, ) -> Result { - result((|| { - let keys = converted(reply)?; - let Phase::ObjectKeys(start) = std::mem::replace(&mut self.0.phase, Phase::GapString) - else { - return Err(RuntimeError::Invariant("JSON stringify unexpected keys reply").into()); - }; - self.0 - .enumerate(runtime, start, keys.into_iter(), Vec::new()) - })()) + result( + runtime, + (|| { + let keys = converted(reply)?; + let Phase::ObjectKeys(start) = + std::mem::replace(&mut self.0.phase, Phase::GapString) + else { + return Err( + RuntimeError::Invariant("JSON stringify unexpected keys reply").into(), + ); + }; + self.0 + .enumerate(runtime, start, keys.into_iter(), Vec::new()) + })(), + ) } pub(crate) fn boolean( mut self, runtime: &Runtime, reply: NativeConversion, ) -> Result { - result((|| { - let value = converted(reply)?; - let Phase::Enumerable { - start, - remaining, - mut keys, - key, - } = std::mem::replace(&mut self.0.phase, Phase::GapString) - else { - return Err( - RuntimeError::Invariant("JSON stringify unexpected boolean reply").into(), - ); - }; - if value { - keys.push(runtime.0.state.borrow().atoms.to_js_string(key.atom())?); - } - self.0.enumerate(runtime, start, remaining, keys) - })()) + result( + runtime, + (|| { + let value = converted(reply)?; + let Phase::Enumerable { + start, + remaining, + mut keys, + key, + } = std::mem::replace(&mut self.0.phase, Phase::GapString) + else { + return Err( + RuntimeError::Invariant("JSON stringify unexpected boolean reply").into(), + ); + }; + if value { + keys.push(runtime.0.state.borrow().atoms.to_js_string(key.atom())?); + } + self.0.enumerate(runtime, start, remaining, keys) + })(), + ) } } pub(super) fn finish( @@ -888,7 +957,7 @@ pub(super) fn finish( step = match step { StringifyStep::Complete(result) => return Ok(result), StringifyStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -896,11 +965,11 @@ pub(super) fn finish( )? } StringifyStep::String { mut resume } => { - let value = resume.take_string_value(); + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } StringifyStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } StringifyStep::Keys { mut resume } => { @@ -917,8 +986,12 @@ pub(super) fn finish( } StringifyStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -949,30 +1022,42 @@ mod ownership_tests { let to_json_id = to_json_object.object_id(); let arguments = NativeArguments { actual_arg_count: 2, - readable: vec![root, replacer, Value::Undefined], + readable: vec![ + runtime.into_jsvalue(root).unwrap(), + runtime.into_jsvalue(replacer).unwrap(), + JsValue::Undefined, + ], }; let StringifyStep::Read { mut resume } = StringifyStep::start(&runtime, context.realm, &arguments).unwrap() else { panic!("expected root toJSON lookup"); }; - let _ = resume.take_read_receiver(); + runtime + .release_jsvalue(resume.take_read_receiver()) + .unwrap(); let _ = resume.take_read_key(); let resident_owner = (&*resume.0) as *const StringifyResumeState; - drop(arguments); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } let StringifyStep::Call { mut resume } = resume - .resume(&runtime, Completion::Return(Value::Undefined)) + .resume(&runtime, Completion::Return(JsValue::Undefined)) .unwrap() else { panic!("expected root replacer"); }; let _ = resume.take_call_callable(); - let _ = resume.take_call_receiver(); + runtime + .release_jsvalue(resume.take_call_receiver()) + .unwrap(); let arguments = resume.take_call_arguments(); assert_eq!(resident_owner, (&*resume.0) as *const StringifyResumeState); - let root = arguments[1].clone(); - drop(arguments); + let root = runtime.dup_jsvalue(&arguments[1]).unwrap(); + for value in arguments { + runtime.release_jsvalue(value).unwrap(); + } let StringifyStep::Keys { mut resume } = resume.resume(&runtime, Completion::Return(root)).unwrap() else { @@ -996,17 +1081,20 @@ mod ownership_tests { else { panic!("expected property read"); }; - let Value::Object(root) = resume.take_read_receiver() else { + let Value::Object(root) = runtime + .root_and_release_jsvalue(resume.take_read_receiver()) + .unwrap() + else { panic!("expected payload"); }; let key = resume.take_read_key(); let child = runtime .get_property_in_realm(context.realm, &root, &key) .unwrap(); - let Completion::Return(Value::Object(child_object)) = &child else { + let Completion::Return(JsValue::Object(child_object)) = &child else { panic!("expected child"); }; - let child_id = child_object.object_id(); + let child_id = *child_object; runtime .internal_delete_property(context.realm, &root, &key) .unwrap(); @@ -1015,11 +1103,16 @@ mod ownership_tests { let StringifyStep::Read { mut resume } = resume.resume(&runtime, child).unwrap() else { panic!("expected child toJSON lookup"); }; - let _ = resume.take_read_receiver(); + runtime + .release_jsvalue(resume.take_read_receiver()) + .unwrap(); let _ = resume.take_read_key(); let step = resume - .resume(&runtime, Completion::Return(to_json)) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(to_json).unwrap()), + ) .unwrap(); assert!(matches!(step, StringifyStep::Call { .. })); runtime.run_gc().unwrap(); @@ -1040,20 +1133,20 @@ mod ownership_tests { #[derive(Default)] struct StringifyStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, - string_value: Option, - number_value: Option, + string_value: Option, + number_value: Option, keys_object: Option, enumerable_object: Option, enumerable_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, } impl StringifyStep { pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: StringifyResume, ) -> Self { @@ -1061,11 +1154,11 @@ impl StringifyStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_string(value: Value, mut resume: StringifyResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: StringifyResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } - pub(crate) fn request_number(value: Value, mut resume: StringifyResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: StringifyResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -1084,8 +1177,8 @@ impl StringifyStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: StringifyResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -1095,7 +1188,7 @@ impl StringifyStep { } } impl StringifyResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -1109,14 +1202,14 @@ impl StringifyResume { .take() .expect("StringifyStep Read key") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value .take() .expect("StringifyStep String value") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -1151,14 +1244,14 @@ impl StringifyResume { .take() .expect("StringifyStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("StringifyStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/json/tests.rs b/src/engine/builtins/json/tests.rs index ae0147b4..d897fb7f 100644 --- a/src/engine/builtins/json/tests.rs +++ b/src/engine/builtins/json/tests.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeCProto; use crate::engine::value::conversion::NativeConversion; @@ -32,7 +33,8 @@ fn global_json_is_realm_aware_lazy_and_reserves_the_pinned_table_order() { let state = runtime.0.state.borrow(); let object = state.heap.object(global.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot].flags, PropertyFlags::data(true, false, true), @@ -63,7 +65,8 @@ fn global_json_is_realm_aware_lazy_and_reserves_the_pinned_table_order() { let state = runtime.0.state.borrow(); let object = state.heap.object(json.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot = usize::try_from(shape.find(method.atom()).unwrap()).unwrap(); + let slot = + usize::try_from(shape.find(AtomIdx::from_raw(method.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot].flags, PropertyFlags::data(true, false, true), diff --git a/src/engine/builtins/map.rs b/src/engine/builtins/map.rs index da73934e..27317a0f 100644 --- a/src/engine/builtins/map.rs +++ b/src/engine/builtins/map.rs @@ -19,7 +19,7 @@ use crate::engine::object::{ WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation, NativeInvokeOutcome}; #[cfg(test)] @@ -100,13 +100,17 @@ impl Runtime { let entries_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Entries)?; let entries = match self.get_property_in_realm(realm, &map_prototype, &entries_key)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) => { + Completion::Return(value @ JsValue::Object(_)) => { + self.root_and_release_jsvalue(value)? + } + Completion::Return(value) => { + self.release_jsvalue(value)?; return Err(RuntimeError::Invariant( "Map.prototype.entries was not callable during bootstrap", )); } - Completion::Throw(_) => { + Completion::Throw(value) => { + self.release_jsvalue(value)?; return Err(RuntimeError::Invariant( "Map.prototype.entries initialization threw during bootstrap", )); @@ -279,7 +283,9 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.call_map_native_borrowed(realm, kind, &invocation, arguments) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.call_map_native_borrowed(realm, kind, invocation, arguments) + }) } pub(crate) fn call_map_native_borrowed( &self, @@ -317,7 +323,7 @@ impl Runtime { "Map species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } fn call_map_constructor( @@ -339,12 +345,12 @@ impl Runtime { ) } - fn map_receiver<'a>( + fn map_receiver( &self, realm: ContextId, - invocation: &'a NativeInvocation, + invocation: &NativeInvocation, getter: bool, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { let this_value = match (getter, invocation) { (false, NativeInvocation::Call { this_value }) | (true, NativeInvocation::Getter { this_value }) => this_value, @@ -354,13 +360,14 @@ impl Runtime { )); } }; - let Value::Object(object) = this_value else { + let JsValue::Object(id) = this_value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, "Map object expected", )?)); }; + let object = ObjectRef::from_borrowed_handle(self.clone(), *id)?; if !object.belongs_to(self) { return Err(RuntimeError::WrongRuntime("Map receiver")); } @@ -383,9 +390,9 @@ impl Runtime { Ok(NativeConversion::Value(object)) } - pub(in crate::engine::builtins) fn normalized_map_key(value: Value) -> Value { + pub(in crate::engine::builtins) fn normalized_map_key(value: JsValue) -> JsValue { match value { - Value::Float(0.0) => Value::Int(0), + JsValue::Float(0.0) => JsValue::Int(0), value => value, } } @@ -393,9 +400,9 @@ impl Runtime { pub(in crate::engine::builtins) fn find_map_record( &self, map: &ObjectRef, - key: &Value, + key: &JsValue, ) -> Result, RuntimeError> { - let raw_key = self.raw_property_value(key)?; + let raw_key = key.as_raw(); let state = self.0.state.borrow(); let heap = &state.heap; let Some(index) = heap.map_find_record(map.object_id(), &raw_key)? else { @@ -413,14 +420,14 @@ impl Runtime { pub(in crate::engine::builtins) fn set_map_record( &self, map: &ObjectRef, - key: Value, - value: Value, + key: JsValue, + value: JsValue, ) -> Result<(), RuntimeError> { - self.validate_value_domain(&key, "Map key")?; - self.validate_value_domain(&value, "Map value")?; let key = Self::normalized_map_key(key); - let raw_key = self.raw_property_value(&key)?; - let raw_value = self.raw_property_value(&value)?; + let raw_key = key.as_raw(); + let raw_value = value.as_raw(); + // The record retains its own copy edges inside the heap transaction; + // the caller-owned key/value edges are released on every exit below. let mut state = self.0.state.borrow_mut(); let existing = state.heap.map_find_record(map.object_id(), &raw_key)?; let retained = if existing.is_some() { @@ -441,24 +448,30 @@ impl Runtime { Ok(cleanup) => cleanup, Err(error) => { state.release_atoms(retained)?; + drop(state); + self.release_jsvalue(key)?; + self.release_jsvalue(value)?; return Err(error.into()); } }; state.apply_cleanup(cleanup)?; drop(state); - drop(key); - drop(value); + self.release_jsvalue(key)?; + self.release_jsvalue(value)?; Ok(()) } - fn delete_map_record(&self, map: &ObjectRef, key: &Value) -> Result { - let key = Self::normalized_map_key(key.clone()); + fn delete_map_record(&self, map: &ObjectRef, key: JsValue) -> Result { + let key = Self::normalized_map_key(key); let Some((index, _)) = self.find_map_record(map, &key)? else { + self.release_jsvalue(key)?; return Ok(false); }; let mut state = self.0.state.borrow_mut(); let cleanup = state.heap.map_delete_record(map.object_id(), index)?; state.apply_cleanup(cleanup)?; + drop(state); + self.release_jsvalue(key)?; Ok(true) } @@ -470,24 +483,18 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let key = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Map.prototype.set key argv was not padded", - ))?; - let value = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Map.prototype.set value argv was not padded", - ))?; - self.set_map_record(map, key, value)?; - Ok(Completion::Return(Value::Object(map.clone()))) + let key = self.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Map.prototype.set key argv was not padded"), + )?)?; + let value = self.dup_jsvalue(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("Map.prototype.set value argv was not padded"), + )?)?; + self.set_map_record(&map, key, value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object(map))?)) } fn call_map_get( @@ -498,16 +505,19 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let key = Self::normalized_map_key(arguments.readable.first().cloned().ok_or( + let key = Self::normalized_map_key(self.dup_jsvalue(arguments.readable.first().ok_or( RuntimeError::Invariant("Map.prototype.get key argv was not padded"), - )?); - let value = match self.find_map_record(map, &key)? { + )?)?); + let value = match self.find_map_record(&map, &key)? { Some((_, value)) => self.root_raw_value(&value)?, None => Value::Undefined, }; - Ok(Completion::Return(value)) + self.release_jsvalue(key)?; + Ok(Completion::Return(self.into_jsvalue(value)?)) } fn call_map_has( @@ -518,14 +528,16 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let key = Self::normalized_map_key(arguments.readable.first().cloned().ok_or( + let key = Self::normalized_map_key(self.dup_jsvalue(arguments.readable.first().ok_or( RuntimeError::Invariant("Map.prototype.has key argv was not padded"), - )?); - Ok(Completion::Return(Value::Bool( - self.find_map_record(map, &key)?.is_some(), - ))) + )?)?); + let has = self.find_map_record(&map, &key)?.is_some(); + self.release_jsvalue(key)?; + Ok(Completion::Return(JsValue::Bool(has))) } fn call_map_delete( @@ -536,17 +548,15 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let key = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Map.prototype.delete key argv was not padded", - ))?; - Ok(Completion::Return(Value::Bool( - self.delete_map_record(map, &key)?, + let key = self.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Map.prototype.delete key argv was not padded"), + )?)?; + Ok(Completion::Return(JsValue::Bool( + self.delete_map_record(&map, key)?, ))) } @@ -557,12 +567,14 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let mut state = self.0.state.borrow_mut(); let cleanup = state.heap.map_clear(map.object_id())?; state.apply_cleanup(cleanup)?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } fn call_map_size( @@ -572,10 +584,12 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, true)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let size = self.0.state.borrow().heap.map_size(map.object_id())?; - Ok(Completion::Return(Value::number(size as f64))) + Ok(Completion::Return(JsValue::Int(size as i32))) } fn call_map_get_or_insert( @@ -654,11 +668,13 @@ impl Runtime { ) -> Result { let map = match self.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - Ok(Completion::Return(Value::Object( - self.new_map_iterator(realm, map, kind)?, - ))) + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_map_iterator(realm, &map, kind)?, + ))?)) } pub(crate) fn call_map_iterator_next( @@ -668,9 +684,12 @@ impl Runtime { ) -> Result { match self.call_map_iterator_next_raw(realm, invocation)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_iterator_result(realm, value, done)?, + ))?)) + } } } @@ -684,9 +703,9 @@ impl Runtime { "Map Iterator next did not receive an iterator-next invocation", )); }; - let Value::Object(iterator) = this_value else { + let JsValue::Object(iterator_id) = this_value else { return Ok(NativeInvokeOutcome::Completion(Completion::Throw( - self.new_native_error( + self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "Map Iterator object expected", @@ -698,12 +717,12 @@ impl Runtime { .state .borrow_mut() .heap - .begin_map_iterator_next(iterator.object_id()); + .begin_map_iterator_next(iterator_id); let (map, mut index, kind) = match state { Ok(state) => state, Err(HeapError::Invariant(_)) => { return Ok(NativeInvokeOutcome::Completion(Completion::Throw( - self.new_native_error( + self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "Map Iterator object expected", @@ -714,7 +733,7 @@ impl Runtime { }; let Some(map_id) = map else { return Ok(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }); }; @@ -728,10 +747,10 @@ impl Runtime { .map(|(id, record)| (id, record.key.clone(), record.value.clone())); let Some((record_index, key, value)) = record else { let mut state = self.0.state.borrow_mut(); - let cleanup = state.heap.finish_map_iterator(iterator.object_id())?; + let cleanup = state.heap.finish_map_iterator(iterator_id)?; state.apply_cleanup(cleanup)?; return Ok(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }); }; @@ -742,19 +761,23 @@ impl Runtime { .state .borrow_mut() .heap - .set_map_iterator_index(iterator.object_id(), index)?; + .set_map_iterator_index(iterator_id, index)?; self.0 .state .borrow_mut() .heap - .set_map_iterator_current(iterator.object_id(), record_index)?; + .set_map_iterator_current(iterator_id, record_index)?; let key = self.root_raw_value(&key)?; let value = match kind { - MapIteratorKind::Key => key, - MapIteratorKind::Value => self.root_raw_value(&value)?, - MapIteratorKind::KeyAndValue => Value::Object( - self.new_array_from_values(realm, vec![key, self.root_raw_value(&value)?])?, - ), + MapIteratorKind::Key => self.into_jsvalue(key)?, + MapIteratorKind::Value => self.into_jsvalue(self.root_raw_value(&value)?)?, + MapIteratorKind::KeyAndValue => { + let key = self.into_jsvalue(key)?; + let value = self.into_jsvalue(self.root_raw_value(&value)?)?; + self.into_jsvalue(Value::Object( + self.new_array_from_values_jsvalue(realm, vec![key, value])?, + ))? + } }; Ok(NativeInvokeOutcome::IteratorNextRaw { value, done: false }) } diff --git a/src/engine/builtins/map/callback.rs b/src/engine/builtins/map/callback.rs index 1fb8a753..1650cf37 100644 --- a/src/engine/builtins/map/callback.rs +++ b/src/engine/builtins/map/callback.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{CallableRef, ObjectRef}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -33,18 +33,45 @@ impl std::ops::DerefMut for CallbackResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct CallbackResumeState { + runtime: Runtime, pending_effect: CallbackStepPending, phase: Phase, map: ObjectRef, } +impl Drop for CallbackResumeState { + /// Release the internal edges the pending effect and phase still own when + /// the request is abandoned. Consumption goes through `Option::take` or + /// `mem::replace`, so drained fields are inert here; releases are + /// defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + match &mut self.phase { + Phase::Each { receiver, .. } => { + let value = std::mem::replace(receiver, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(value); + } + Phase::Insert(key) => { + let value = std::mem::replace(key, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(value); + } + } + } +} enum Phase { Each { callback: CallableRef, - receiver: Value, + receiver: JsValue, index: usize, record: Option, }, - Insert(Value), + Insert(JsValue), } impl CallbackStep { pub(crate) fn start( @@ -56,66 +83,81 @@ impl CallbackStep { ) -> Result { let map = match runtime.map_receiver(realm, invocation, false)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; if let CallbackKind::Insert { computed } = kind { - let key = Runtime::normalized_map_key(arguments.readable.first().cloned().ok_or( - RuntimeError::Invariant("Map getOrInsert key argv was not padded"), + let key = Runtime::normalized_map_key(runtime.dup_jsvalue( + arguments.readable.first().ok_or(RuntimeError::Invariant( + "Map getOrInsert key argv was not padded", + ))?, )?); - let second = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Map getOrInsert value argv was not padded", - ))?; + let second = runtime.dup_jsvalue(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("Map getOrInsert value argv was not padded"), + )?)?; let callback = if computed { match callable(runtime, realm, &second)? { NativeConversion::Value(value) => Some(value), NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + runtime.release_jsvalue(key)?; + runtime.release_jsvalue(second)?; + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } else { None }; - if let Some((_, value)) = runtime.find_map_record(map, &key)? { + if let Some((_, value)) = runtime.find_map_record(&map, &key)? { + runtime.release_jsvalue(key)?; + runtime.release_jsvalue(second)?; return Ok(Self::Complete(Completion::Return( - runtime.root_raw_value(&value)?, + runtime.into_jsvalue(runtime.root_raw_value(&value)?)?, ))); } if let Some(callable) = callback { + let call_key = runtime.dup_jsvalue(&key)?; return Ok(Self::request_call( callable, - Value::Undefined, - vec![key.clone()], + JsValue::Undefined, + vec![call_key], CallbackResume(Box::new(CallbackResumeState { + runtime: runtime.clone(), pending_effect: CallbackStepPending::default(), map: map.clone(), phase: Phase::Insert(key), })), )); } - runtime.set_map_record(map, key, second.clone())?; - return Ok(Self::Complete(Completion::Return(second))); + let result = runtime.dup_jsvalue(&second); + runtime.set_map_record(&map, key, second)?; + return Ok(Self::Complete(Completion::Return(result?))); } let value = arguments.readable.first().ok_or(RuntimeError::Invariant( "Map.prototype.forEach callback argv was not padded", ))?; let callback = match callable(runtime, realm, value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; CallbackResume(Box::new(CallbackResumeState { + runtime: runtime.clone(), pending_effect: CallbackStepPending::default(), map: map.clone(), phase: Phase::Each { callback, - receiver: arguments - .readable - .get(1) - .cloned() - .unwrap_or(Value::Undefined), + receiver: match arguments.readable.get(1) { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, + }, index: 0, record: None, }, @@ -126,10 +168,12 @@ impl CallbackStep { fn callable( runtime: &Runtime, realm: ContextId, - value: &Value, + value: &JsValue, ) -> Result, RuntimeError> { let result = match value { - Value::Object(object) => runtime.as_callable(object)?, + JsValue::Object(id) => { + runtime.as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)? + } _ => None, }; Ok(match result { @@ -163,13 +207,15 @@ impl CallbackResume { .map(|(id, entry)| (id, entry.key.clone(), entry.value.clone())) }; let Some((record_index, key, value)) = entry else { - return Ok(CallbackStep::Complete(Completion::Return(Value::Undefined))); + return Ok(CallbackStep::Complete(Completion::Return( + JsValue::Undefined, + ))); }; *index = record_index.checked_add(1).ok_or(RuntimeError::Invariant( "Map forEach record index overflowed", ))?; - let key = runtime.root_raw_value(&key)?; - let value = runtime.root_raw_value(&value)?; + let key = runtime.into_jsvalue(runtime.root_raw_value(&key)?)?; + let value = runtime.into_jsvalue(runtime.root_raw_value(&value)?)?; *record = Some( runtime.push_active_collection_record(ActiveCollectionRecord::Map { object: self.0.map.object_id(), @@ -178,8 +224,12 @@ impl CallbackResume { ); Ok(CallbackStep::request_call( callback.clone(), - receiver.clone(), - vec![value, key, Value::Object(self.0.map.clone())], + runtime.dup_jsvalue(receiver)?, + vec![ + value, + key, + runtime.into_jsvalue(crate::engine::value::Value::Object(self.0.map.clone()))?, + ], self, )) } @@ -199,14 +249,17 @@ impl CallbackResume { return Ok(CallbackStep::Complete(Completion::Throw(value))); } }; - match self.0.phase { - Phase::Insert(key) => { - runtime.delete_map_record(&self.0.map, &key)?; - runtime.set_map_record(&self.0.map, key, value.clone())?; - Ok(CallbackStep::Complete(Completion::Return(value))) - } - Phase::Each { .. } => self.next(runtime), + if matches!(self.0.phase, Phase::Each { .. }) { + return self.next(runtime); } + let key = match &mut self.0.phase { + Phase::Insert(key) => std::mem::replace(key, JsValue::Undefined), + Phase::Each { .. } => unreachable!("Map callback phase changed during resume"), + }; + let result = runtime.dup_jsvalue(&value)?; + runtime.delete_map_record(&self.0.map, runtime.dup_jsvalue(&key)?)?; + runtime.set_map_record(&self.0.map, key, value)?; + Ok(CallbackStep::Complete(Completion::Return(result))) } } pub(crate) fn finish( @@ -223,7 +276,15 @@ pub(crate) fn finish( let arguments = resume.take_call_arguments(); resume.resume( runtime, - runtime.call_internal(realm, &callable, receiver, &arguments)?, + runtime.call_internal( + realm, + &callable, + runtime.root_and_release_jsvalue(receiver)?, + &arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?, + )?, )? } }; @@ -233,6 +294,7 @@ pub(crate) fn finish( #[cfg(test)] mod tests { use super::*; + use crate::engine::value::Value; #[test] fn abandoned_each_requests_release_records_and_collection_roots_in_lifo_order() { @@ -249,24 +311,30 @@ mod tests { let set_id = set.object_id(); let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![context.eval("(function () {})").unwrap()], + readable: vec![ + runtime + .unroot_value(&context.eval("(function () {})").unwrap()) + .unwrap(), + ], + }; + let map_invocation = NativeInvocation::Call { + this_value: runtime.unroot_value(&Value::Object(map)).unwrap(), + }; + let set_invocation = NativeInvocation::Call { + this_value: runtime.unroot_value(&Value::Object(set)).unwrap(), }; let map_step = CallbackStep::start( &runtime, context.realm, CallbackKind::Each, - &NativeInvocation::Call { - this_value: Value::Object(map), - }, + &map_invocation, &arguments, ) .unwrap(); let set_step = crate::engine::builtins::set::callback::EachStep::start( &runtime, context.realm, - &NativeInvocation::Call { - this_value: Value::Object(set), - }, + &set_invocation, &arguments, ) .unwrap(); @@ -275,7 +343,19 @@ mod tests { set_step, crate::engine::builtins::set::callback::EachStep::Call { .. } )); - drop(arguments); + { + let NativeInvocation::Call { this_value } = map_invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + let NativeInvocation::Call { this_value } = set_invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + } runtime.run_gc().unwrap(); { let state = runtime.0.state.borrow(); @@ -307,14 +387,14 @@ mod tests { #[derive(Default)] struct CallbackStepPending { call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, } impl CallbackStep { pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: CallbackResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -331,14 +411,14 @@ impl CallbackResume { .take() .expect("CallbackStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("CallbackStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/math.rs b/src/engine/builtins/math.rs index ecced205..0f5b2f72 100644 --- a/src/engine/builtins/math.rs +++ b/src/engine/builtins/math.rs @@ -15,7 +15,7 @@ use crate::engine::object::shape::PropertyFlags; use crate::engine::object::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -579,17 +579,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::MathStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::MathKind::MinMax(selector), - &invocation, - arguments, - )?, - ) + operation::MathStep::start( + self, + realm, + operation::MathKind::MinMax(selector), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_math_unary( @@ -599,17 +601,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::MathStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::MathKind::Unary(selector), - &invocation, - arguments, - )?, - ) + operation::MathStep::start( + self, + realm, + operation::MathKind::Unary(selector), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_math_binary( @@ -619,17 +623,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::MathStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::MathKind::Binary(selector), - &invocation, - arguments, - )?, - ) + operation::MathStep::start( + self, + realm, + operation::MathKind::Binary(selector), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_math_hypot( @@ -638,17 +644,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::MathStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::MathKind::Hypot, - &invocation, - arguments, - )?, - ) + operation::MathStep::start( + self, + realm, + operation::MathKind::Hypot, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_math_random( @@ -662,7 +670,7 @@ impl Runtime { )); }; let random = self.0.state.borrow_mut().heap.next_math_random_u64(realm)?; - Ok(Completion::Return(Value::Float(quickjs_random_fraction( + Ok(Completion::Return(JsValue::Float(quickjs_random_fraction( random, )))) } @@ -673,17 +681,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::MathStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::MathKind::Imul, - &invocation, - arguments, - )?, - ) + operation::MathStep::start( + self, + realm, + operation::MathKind::Imul, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_math_clz32( @@ -692,17 +702,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::finish( - self, - realm, - operation::MathStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::finish( self, realm, - operation::MathKind::Clz32, - &invocation, - arguments, - )?, - ) + operation::MathStep::start( + self, + realm, + operation::MathKind::Clz32, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_math_sum_precise( @@ -711,10 +723,12 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - sum::finish( - self, - realm, - sum::SumStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + sum::finish( + self, + realm, + sum::SumStep::start(self, realm, invocation, arguments)?, + ) + }) } } diff --git a/src/engine/builtins/math/operation.rs b/src/engine/builtins/math/operation.rs index 5c8911a7..67fd0089 100644 --- a/src/engine/builtins/math/operation.rs +++ b/src/engine/builtins/math/operation.rs @@ -12,7 +12,7 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, builtins::native::{MathBinaryKind, MathMinMaxKind, MathUnaryKind}, heap::ContextId, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion, number::operations::Number}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -42,7 +42,7 @@ impl MathKind { } pub(crate) enum MathStep { Complete(Completion), - Number { value: Value, resume: MathResume }, + Number { value: JsValue, resume: MathResume }, } pub(crate) struct MathResume(Box); impl std::ops::Deref for MathResume { @@ -59,7 +59,7 @@ impl std::ops::DerefMut for MathResume { const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct MathResumeState { kind: MathKind, - arguments: std::vec::IntoIter, + arguments: std::vec::IntoIter, result: Option, count: usize, } @@ -92,10 +92,18 @@ impl MathStep { count, }; for (index, value) in values.iter().enumerate() { - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { // The native activation owns original argv. A suspended // continuation needs only the not-yet-converted suffix. - let remaining = values[index..].to_vec(); + let mut remaining = Vec::new(); + remaining + .try_reserve_exact(values.len() - index) + .map_err(|_| { + RuntimeError::Invariant("Math remaining argv allocation failed") + })?; + for remaining_value in &values[index..] { + remaining.push(runtime.dup_jsvalue(remaining_value)?); + } resume.arguments = remaining.into_iter(); #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( @@ -106,8 +114,9 @@ impl MathStep { // Object arguments above retain the shared waiting protocol. // NativeActivation already owns this primitive: borrow it in // the same conversion kernel used by NumberStep completion. - let result = runtime.number_from_primitive(realm, value)?; - if let Some(completion) = resume.accept_number(result)? { + let owned = runtime.root_value(value)?; + let result = runtime.number_from_primitive(realm, &owned)?; + if let Some(completion) = resume.accept_number(runtime, result)? { #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( "math_completed_without_argument_storage", @@ -135,9 +144,10 @@ impl MathResume { } pub(crate) fn number( mut self, + runtime: &Runtime, result: NativeConversion, ) -> Result { - if let Some(completion) = self.accept_number(result)? { + if let Some(completion) = self.accept_number(runtime, result)? { Ok(MathStep::Complete(completion)) } else { self.next() @@ -148,7 +158,7 @@ impl MathResumeState { fn finish(self) -> Result { let value = match self.kind { MathKind::MinMax(kind) if self.result.is_none() => { - return Ok(MathStep::Complete(Completion::Return(Value::Float( + return Ok(MathStep::Complete(Completion::Return(JsValue::Float( match kind { MathMinMaxKind::Min => f64::INFINITY, MathMinMaxKind::Max => f64::NEG_INFINITY, @@ -156,7 +166,7 @@ impl MathResumeState { )))); } MathKind::Hypot if self.count == 0 => { - return Ok(MathStep::Complete(Completion::Return(Value::Int(0)))); + return Ok(MathStep::Complete(Completion::Return(JsValue::Int(0)))); } MathKind::Hypot if self.count == 1 => self .result @@ -166,23 +176,26 @@ impl MathResumeState { .result .ok_or(RuntimeError::Invariant("Math result missing"))?, }; - Ok(MathStep::Complete(Completion::Return(Value::number(value)))) + Ok(MathStep::Complete(Completion::Return( + Number::compact(value).into(), + ))) } /// One numerical accumulation kernel for immediate and suspended inputs. fn accept_number( &mut self, + runtime: &Runtime, result: NativeConversion, ) -> Result, RuntimeError> { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Some(Completion::Throw(value))); + return Ok(Some(Completion::Throw(runtime.into_jsvalue(value)?))); } }; self.result = Some(match self.kind { MathKind::Unary(kind) => quickjs_unary(kind, value), MathKind::Clz32 => { - return Ok(Some(Completion::Return(Value::Int( + return Ok(Some(Completion::Return(JsValue::Int( Runtime::to_uint32_number(value).leading_zeros() as i32, )))); } @@ -197,7 +210,7 @@ impl MathResumeState { if let Some(left) = self.result { let product = Runtime::to_uint32_number(left) .wrapping_mul(Runtime::to_uint32_number(value)); - return Ok(Some(Completion::Return(Value::Int(i32::from_ne_bytes( + return Ok(Some(Completion::Return(JsValue::Int(i32::from_ne_bytes( product.to_ne_bytes(), ))))); } else { @@ -240,7 +253,8 @@ pub(crate) fn finish( step = match step { MathStep::Complete(result) => return Ok(result), MathStep::Number { value, resume } => { - resume.number(runtime.native_to_number(realm, &value)?)? + let owned = runtime.root_and_release_jsvalue(value)?; + resume.number(runtime, runtime.native_to_number(realm, &owned)?)? } }; } diff --git a/src/engine/builtins/math/sum.rs b/src/engine/builtins/math/sum.rs index 539eb2d5..d44904c2 100644 --- a/src/engine/builtins/math/sum.rs +++ b/src/engine/builtins/math/sum.rs @@ -8,7 +8,7 @@ use crate::engine::{ }, heap::ContextId, object::{CallableRef, ObjectRef, PropertyKey, WellKnownSymbol}, - value::Value, + value::{JsValue, Value}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -44,9 +44,9 @@ pub(crate) struct SumResumeState { pending_effect: SumStepPending, realm: ContextId, phase: Phase, - iterable: Value, + iterable: JsValue, iterator: Option, - next: Value, + next: JsValue, sum: SumPrecise, } impl SumStep { @@ -61,16 +61,12 @@ impl SumStep { "Math.sumPrecise requires generic invocation", )); } - let iterable = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Math.sumPrecise argv was not padded", - ))?; + let iterable = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Math.sumPrecise argv was not padded"), + )?)?; if matches!(iterable, Value::Null | Value::Undefined) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!( @@ -84,8 +80,9 @@ impl SumStep { )?, ))); } + let iterable = runtime.into_jsvalue(iterable)?; Ok({ - let __pending_field_receiver = iterable.clone(); + let __pending_field_receiver = runtime.dup_jsvalue(&iterable)?; let __pending_field_key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)); let __pending_field_resume = SumResume(Box::new(SumResumeState { @@ -94,7 +91,7 @@ impl SumStep { phase: Phase::Method, iterable, iterator: None, - next: Value::Undefined, + next: JsValue::Undefined, sum: SumPrecise::new(), })); Self::request_read( @@ -112,7 +109,7 @@ impl SumResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(SumStep::Complete(Completion::Throw(value))), }; match self.0.phase { @@ -123,7 +120,7 @@ impl SumResume { }; let Some(callable) = callable else { return Ok(SumStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "value is not iterable", @@ -133,7 +130,7 @@ impl SumResume { self.0.phase = Phase::Iterator; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = self.0.iterable.clone(); + let __pending_field_receiver = runtime.dup_jsvalue(&self.0.iterable)?; let __pending_field_resume = self; SumStep::request_call( __pending_field_callable, @@ -145,18 +142,18 @@ impl SumResume { Phase::Iterator => { let Value::Object(iterator) = value else { return Ok(SumStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", )?, ))); }; - self.0.iterable = Value::Undefined; + self.0.iterable = JsValue::Undefined; self.0.iterator = Some(iterator.clone()); self.0.phase = Phase::NextMethod; Ok({ - let __pending_field_receiver = Value::Object(iterator); + let __pending_field_receiver = JsValue::Object(iterator.into_handle()); let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?; let __pending_field_resume = self; @@ -168,7 +165,7 @@ impl SumResume { }) } Phase::NextMethod => { - self.0.next = value; + self.0.next = runtime.into_jsvalue(value)?; self.next() } _ => Err(RuntimeError::Invariant("Math sum value phase mismatch")), @@ -182,7 +179,7 @@ impl SumResume { .iterator .clone() .ok_or(RuntimeError::Invariant("Math sum iterator missing"))?; - let __pending_field_next = self.0.next.clone(); + let __pending_field_next = std::mem::replace(&mut self.0.next, JsValue::Undefined); let __pending_field_resume = self; SumStep::request_next( __pending_field_iterator, @@ -202,7 +199,7 @@ impl SumResume { let item = match result { ObjectIteratorStep::Yield(value) => value, ObjectIteratorStep::Done => { - return Ok(SumStep::Complete(Completion::Return(Value::Float( + return Ok(SumStep::Complete(Completion::Return(JsValue::Float( self.0.sum.result(), )))); } @@ -211,8 +208,8 @@ impl SumResume { } }; let number = match item { - Value::Int(value) => f64::from(value), - Value::Float(value) => value, + JsValue::Int(value) => f64::from(value), + JsValue::Float(value) => value, _ => { return Ok({ let __pending_field_iterator = self @@ -220,11 +217,12 @@ impl SumResume { .iterator .take() .ok_or(RuntimeError::Invariant("Math sum iterator missing"))?; - let __pending_field_completion = Completion::Throw(runtime.new_native_error( - self.0.realm, - NativeErrorKind::Type, - "not a number", - )?); + let __pending_field_completion = + Completion::Throw(runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "not a number", + )?); let __pending_field_resume = self; SumStep::request_close( __pending_field_iterator, @@ -247,7 +245,7 @@ pub(crate) fn finish( step = match step { SumStep::Complete(result) => return Ok(result), SumStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -256,7 +254,7 @@ pub(crate) fn finish( } SumStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &[])?, @@ -264,7 +262,7 @@ pub(crate) fn finish( } SumStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let next = resume.take_next_next(); + let next = runtime.root_and_release_jsvalue(resume.take_next_next())?; resume.item( runtime, finish_next( @@ -297,11 +295,11 @@ fn sum_resume_keeps_one_resident_owner_across_iterator_transitions() { let callable = context.eval("(function(){})").unwrap(); let object = runtime.new_object(None).unwrap(); let invocation = NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }; let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::Object(object.clone())], + readable: vec![JsValue::Object(object.object_id())], }; let SumStep::Read { mut resume } = SumStep::start(&runtime, context.realm, &invocation, &arguments).unwrap() @@ -312,7 +310,10 @@ fn sum_resume_keeps_one_resident_owner_across_iterator_transitions() { drop(resume.take_read_key()); let address = &*resume.0 as *const SumResumeState; let SumStep::Call { mut resume } = resume - .resume(&runtime, Completion::Return(callable)) + .resume( + &runtime, + Completion::Return(runtime.unroot_value(&callable).unwrap()), + ) .unwrap() else { panic!("iterator call") @@ -321,7 +322,10 @@ fn sum_resume_keeps_one_resident_owner_across_iterator_transitions() { drop(resume.take_call_receiver()); assert_eq!(&*resume.0 as *const SumResumeState, address); let SumStep::Read { mut resume } = resume - .resume(&runtime, Completion::Return(Value::Object(object))) + .resume( + &runtime, + Completion::Return(JsValue::Object(object.object_id())), + ) .unwrap() else { panic!("next read") @@ -333,31 +337,31 @@ fn sum_resume_keeps_one_resident_owner_across_iterator_transitions() { #[derive(Default)] struct SumStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, + call_receiver: Option, next_iterator: Option, - next_next: Option, + next_next: Option, close_iterator: Option, close_completion: Option, } impl SumStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: SumResume) -> Self { + pub(crate) fn request_read(receiver: JsValue, key: PropertyKey, mut resume: SumResume) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, + receiver: JsValue, mut resume: SumResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); resume.0.pending_effect.call_receiver = Some(receiver); Self::Call { resume } } - pub(crate) fn request_next(iterator: ObjectRef, next: Value, mut resume: SumResume) -> Self { + pub(crate) fn request_next(iterator: ObjectRef, next: JsValue, mut resume: SumResume) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); resume.0.pending_effect.next_next = Some(next); Self::Next { resume } @@ -373,7 +377,7 @@ impl SumStep { } } impl SumResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -394,7 +398,7 @@ impl SumResume { .take() .expect("SumStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver @@ -408,7 +412,7 @@ impl SumResume { .take() .expect("SumStep Next iterator") } - pub(crate) fn take_next_next(&mut self) -> Value { + pub(crate) fn take_next_next(&mut self) -> JsValue { self.0 .pending_effect .next_next diff --git a/src/engine/builtins/math/tests.rs b/src/engine/builtins/math/tests.rs index 2d5288e9..a4165e17 100644 --- a/src/engine/builtins/math/tests.rs +++ b/src/engine/builtins/math/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::engine::atom::AtomIdx; use crate::engine::heap::RawValue; fn sum(values: &[f64]) -> f64 { @@ -255,7 +256,8 @@ fn global_math_is_realm_aware_and_materializes_only_on_get() { let state = runtime.0.state.borrow(); let object = state.heap.object(global.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -292,7 +294,8 @@ fn global_math_is_realm_aware_and_materializes_only_on_get() { let state = runtime.0.state.borrow(); let object = state.heap.object(first_global.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::Data(RawValue::Object(id))) if *id == first_math.object_id() diff --git a/src/engine/builtins/object.rs b/src/engine/builtins/object.rs index d96d775b..1237c468 100644 --- a/src/engine/builtins/object.rs +++ b/src/engine/builtins/object.rs @@ -16,7 +16,7 @@ use crate::engine::object::{ OrdinaryPropertyDescriptor, PropertyKey, SymbolRef, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -33,9 +33,9 @@ pub(super) mod string; mod tests; pub(crate) enum ObjectIteratorStep { - Yield(Value), + Yield(JsValue), Done, - Throw(Value), + Throw(JsValue), } impl Runtime { @@ -66,18 +66,20 @@ impl Runtime { arguments: &NativeArguments, element_limit: u64, ) -> Result { - iteration::finish( - self, - realm, - iteration::IterationStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + iteration::finish( self, realm, - iteration::IterationKind::Group, - &invocation, - arguments, - element_limit, - )?, - ) + iteration::IterationStep::start_with_limit( + self, + realm, + iteration::IterationKind::Group, + invocation, + arguments, + element_limit, + )?, + ) + }) } /// QuickJS `js_object_fromEntries`. @@ -94,17 +96,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - iteration::finish( - self, - realm, - iteration::IterationStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + iteration::finish( self, realm, - iteration::IterationKind::Entries, - &invocation, - arguments, - )?, - ) + iteration::IterationStep::start( + self, + realm, + iteration::IterationKind::Entries, + invocation, + arguments, + )?, + ) + }) } /// QuickJS `js_object_hasOwn`. @@ -119,17 +123,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - predicate::finish( - self, - realm, - predicate::PredicateStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + predicate::finish( self, realm, - predicate::PredicateKind::HasOwn, - &invocation, - arguments, - )?, - ) + predicate::PredicateStep::start( + self, + realm, + predicate::PredicateKind::HasOwn, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn initialize_object_prototype_intrinsics( @@ -485,16 +491,18 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - string::finish( - self, - realm, - string::ObjectStringStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + string::finish( self, realm, - string::ObjectStringKind::Tag, - &invocation, - )?, - ) + string::ObjectStringStep::start( + self, + realm, + string::ObjectStringKind::Tag, + invocation, + )?, + ) + }) } pub(crate) fn call_object_prototype_to_locale_string( @@ -502,16 +510,18 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - string::finish( - self, - realm, - string::ObjectStringStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + string::finish( self, realm, - string::ObjectStringKind::Locale, - &invocation, - )?, - ) + string::ObjectStringStep::start( + self, + realm, + string::ObjectStringKind::Locale, + invocation, + )?, + ) + }) } pub(crate) fn call_object_prototype_value_of( @@ -525,41 +535,48 @@ impl Runtime { )); }; match this_value { - value @ Value::Object(_) => Ok(Completion::Return(value)), - Value::Undefined | Value::Null => Ok(Completion::Throw(self.new_native_error( - realm, - NativeErrorKind::Type, - "cannot convert to object", - )?)), - value @ Value::Bool(_) => { + value @ JsValue::Object(_) => Ok(Completion::Return(value)), + JsValue::Undefined | JsValue::Null => { + Ok(Completion::Throw(self.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "cannot convert to object", + )?)) + } + value @ JsValue::Bool(_) => { let prototype = self.primitive_prototype_for_realm(realm, PrimitiveKind::Boolean)?; - Ok(Completion::Return(Value::Object( - self.new_primitive_object(&prototype, PrimitiveKind::Boolean, value)?, + Ok(Completion::Return(JsValue::Object( + self.new_primitive_object_jsvalue(&prototype, PrimitiveKind::Boolean, value)? + .into_handle(), ))) } - value @ (Value::Int(_) | Value::Float(_)) => { + value @ (JsValue::Int(_) | JsValue::Float(_)) => { let prototype = self.primitive_prototype_for_realm(realm, PrimitiveKind::Number)?; - Ok(Completion::Return(Value::Object( - self.new_primitive_object(&prototype, PrimitiveKind::Number, value)?, + Ok(Completion::Return(JsValue::Object( + self.new_primitive_object_jsvalue(&prototype, PrimitiveKind::Number, value)? + .into_handle(), ))) } - value @ Value::String(_) => { + value @ JsValue::String(_) => { let prototype = self.primitive_prototype_for_realm(realm, PrimitiveKind::String)?; - Ok(Completion::Return(Value::Object( - self.new_primitive_object(&prototype, PrimitiveKind::String, value)?, + Ok(Completion::Return(JsValue::Object( + self.new_primitive_object_jsvalue(&prototype, PrimitiveKind::String, value)? + .into_handle(), ))) } - value @ Value::BigInt(_) => { + value @ JsValue::BigInt(_) => { let prototype = self.primitive_prototype_for_realm(realm, PrimitiveKind::BigInt)?; - Ok(Completion::Return(Value::Object( - self.new_primitive_object(&prototype, PrimitiveKind::BigInt, value)?, + Ok(Completion::Return(JsValue::Object( + self.new_primitive_object_jsvalue(&prototype, PrimitiveKind::BigInt, value)? + .into_handle(), ))) } - value @ Value::Symbol(_) => { + value @ JsValue::Symbol(_) => { let prototype = self.primitive_prototype_for_realm(realm, PrimitiveKind::Symbol)?; - Ok(Completion::Return(Value::Object( - self.new_primitive_object(&prototype, PrimitiveKind::Symbol, value)?, + Ok(Completion::Return(JsValue::Object( + self.new_primitive_object_jsvalue(&prototype, PrimitiveKind::Symbol, value)? + .into_handle(), ))) } } @@ -571,11 +588,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - constructor::finish( - self, - realm, - constructor::ObjectConstructorStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + constructor::finish( + self, + realm, + constructor::ObjectConstructorStep::start(self, realm, invocation, arguments)?, + ) + }) } pub(crate) fn call_object_create( @@ -584,11 +603,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object definitions did not receive a generic invocation", )); }; + invocation.release(self)?; definitions::finish( self, realm, @@ -607,11 +628,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object.getPrototypeOf did not receive a generic invocation", )); }; + invocation.release(self)?; prototype::finish( self, realm, @@ -667,11 +690,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object.setPrototypeOf did not receive a generic invocation", )); }; + invocation.release(self)?; prototype::finish( self, realm, @@ -736,11 +761,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object property method did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -759,11 +786,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object definitions did not receive a generic invocation", )); }; + invocation.release(self)?; definitions::finish( self, realm, @@ -783,11 +812,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object enumeration did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -807,11 +838,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object enumeration did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -855,11 +888,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object extensibility method did not receive a generic invocation", )); }; + invocation.release(self)?; let kind = match kind { ObjectExtensibilityKind::IsExtensible => property::PropertyKind::ObjectExtensible, ObjectExtensibilityKind::PreventExtensions => property::PropertyKind::ObjectPrevent, @@ -961,11 +996,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object property method did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -1003,11 +1040,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object enumeration did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -1025,11 +1064,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object.is did not receive a generic invocation", )); }; + invocation.release(self)?; let left = arguments .readable .first() @@ -1038,7 +1079,9 @@ impl Runtime { .readable .get(1) .ok_or(RuntimeError::Invariant("Object.is rhs argv was not padded"))?; - Ok(Completion::Return(Value::Bool(left.same_value(right)))) + let left = self.root_value(left)?; + let right = self.root_value(right)?; + Ok(Completion::Return(JsValue::Bool(left.same_value(&right)))) } pub(crate) fn call_object_assign( @@ -1047,11 +1090,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object.assign did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -1066,11 +1111,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Object integrity method did not receive a generic invocation", )); }; + invocation.release(self)?; property::finish( self, realm, @@ -1089,17 +1136,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - predicate::finish( - self, - realm, - predicate::PredicateStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + predicate::finish( self, realm, - predicate::PredicateKind::PrototypeHasOwn, - &invocation, - arguments, - )?, - ) + predicate::PredicateStep::start( + self, + realm, + predicate::PredicateKind::PrototypeHasOwn, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_object_prototype_property_is_enumerable( @@ -1108,17 +1157,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - predicate::finish( - self, - realm, - predicate::PredicateStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + predicate::finish( self, realm, - predicate::PredicateKind::Enumerable, - &invocation, - arguments, - )?, - ) + predicate::PredicateStep::start( + self, + realm, + predicate::PredicateKind::Enumerable, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_object_prototype_is_prototype_of( @@ -1127,17 +1178,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - prototype::finish( - self, - realm, - prototype::BuiltinPrototypeStep::start_invocation( + self.dispatch_borrowed_invocation(invocation, |invocation| { + prototype::finish( self, realm, - prototype::BuiltinPrototypeKind::IsPrototype, - &invocation, - arguments, - )?, - ) + prototype::BuiltinPrototypeStep::start_invocation( + self, + realm, + prototype::BuiltinPrototypeKind::IsPrototype, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_object_prototype_proto_getter( @@ -1145,20 +1198,22 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - prototype::finish( - self, - realm, - prototype::BuiltinPrototypeStep::start_invocation( + self.dispatch_borrowed_invocation(invocation, |invocation| { + prototype::finish( self, realm, - prototype::BuiltinPrototypeKind::Getter, - &invocation, - &NativeArguments { - actual_arg_count: 0, - readable: Vec::new(), - }, - )?, - ) + prototype::BuiltinPrototypeStep::start_invocation( + self, + realm, + prototype::BuiltinPrototypeKind::Getter, + invocation, + &NativeArguments { + actual_arg_count: 0, + readable: Vec::new(), + }, + )?, + ) + }) } pub(crate) fn call_object_prototype_proto_setter( @@ -1167,17 +1222,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - prototype::finish( - self, - realm, - prototype::BuiltinPrototypeStep::start_invocation( + self.dispatch_borrowed_invocation(invocation, |invocation| { + prototype::finish( self, realm, - prototype::BuiltinPrototypeKind::Setter, - &invocation, - arguments, - )?, - ) + prototype::BuiltinPrototypeStep::start_invocation( + self, + realm, + prototype::BuiltinPrototypeKind::Setter, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_object_prototype_define_accessor( @@ -1187,17 +1244,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - predicate::finish( - self, - realm, - predicate::PredicateStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + predicate::finish( self, realm, - predicate::PredicateKind::Define(kind), - &invocation, - arguments, - )?, - ) + predicate::PredicateStep::start( + self, + realm, + predicate::PredicateKind::Define(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_object_prototype_lookup_accessor( @@ -1207,17 +1266,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - predicate::finish( - self, - realm, - predicate::PredicateStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + predicate::finish( self, realm, - predicate::PredicateKind::Lookup(kind), - &invocation, - arguments, - )?, - ) + predicate::PredicateStep::start( + self, + realm, + predicate::PredicateKind::Lookup(kind), + invocation, + arguments, + )?, + ) + }) } } diff --git a/src/engine/builtins/object/constructor.rs b/src/engine/builtins/object/constructor.rs index 8841c87a..c8e2bd7b 100644 --- a/src/engine/builtins/object/constructor.rs +++ b/src/engine/builtins/object/constructor.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion}, vm::{ Completion, call::{ @@ -15,7 +15,7 @@ use crate::engine::{ pub(crate) enum ObjectConstructorStep { Complete(Completion), Prototype { - new_target: Value, + new_target: JsValue, resume: ObjectConstructorResume, }, } @@ -33,26 +33,28 @@ impl ObjectConstructorStep { )); }; let active = runtime.active_function()?; - let is_active = matches!(new_target, Value::Object(object) if object == &active); - if !matches!(new_target, Value::Undefined) && !is_active { + let is_active = matches!(new_target, JsValue::Object(id) if *id == active.object_id()); + if !matches!(new_target, JsValue::Undefined) && !is_active { return Ok(Self::Prototype { - new_target: new_target.clone(), + new_target: runtime.dup_jsvalue(new_target)?, resume: ObjectConstructorResume, }); } let argument = arguments.readable.first().ok_or(RuntimeError::Invariant( "Object constructor argv was not padded", ))?; - if matches!(argument, Value::Null | Value::Undefined) { + if matches!(argument, JsValue::Null | JsValue::Undefined) { return ObjectConstructorResume.prototype( runtime, NativeConversion::Value(ConstructorPrototypeSource::Realm(realm)), ); } Ok(Self::Complete( - match runtime.native_to_object(realm, argument.clone())? { - NativeConversion::Value(object) => Completion::Return(Value::Object(object)), - NativeConversion::Throw(value) => Completion::Throw(value), + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(argument)?)? { + NativeConversion::Value(object) => { + Completion::Return(JsValue::Object(object.into_handle())) + } + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), }, )) } @@ -65,7 +67,9 @@ impl ObjectConstructorResume { ) -> Result { let prototype = match result { NativeConversion::Throw(value) => { - return Ok(ObjectConstructorStep::Complete(Completion::Throw(value))); + return Ok(ObjectConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(ConstructorPrototypeSource::Explicit(prototype)) => prototype, NativeConversion::Value(ConstructorPrototypeSource::Realm(realm)) => { @@ -80,7 +84,7 @@ impl ObjectConstructorResume { } }; Ok(ObjectConstructorStep::Complete(Completion::Return( - Value::Object(runtime.new_object(Some(&prototype))?), + JsValue::Object(runtime.new_object(Some(&prototype))?.into_handle()), ))) } } @@ -99,7 +103,11 @@ pub(super) fn finish( finish_source( runtime, realm, - ProtoSourceStep::start(runtime, realm, new_target)?, + ProtoSourceStep::start( + runtime, + realm, + runtime.root_and_release_jsvalue(new_target)?, + )?, )?, )?, ), diff --git a/src/engine/builtins/object/copy.rs b/src/engine/builtins/object/copy.rs index c543c110..f7f8385a 100644 --- a/src/engine/builtins/object/copy.rs +++ b/src/engine/builtins/object/copy.rs @@ -5,7 +5,7 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, atom::PropertyKeyKind, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::Completion, }; // These count this cursor's successful logical clone sites, not all runtime @@ -85,7 +85,7 @@ impl CopyStep { "object-rest source was not an Object after ToObject", )); } - return Ok(Self::Complete(Completion::Return(Value::Undefined))); + return Ok(Self::Complete(Completion::Return(JsValue::Undefined))); }; if !target.belongs_to(runtime) || !source.belongs_to(runtime) @@ -140,7 +140,9 @@ impl CopyResume { let keys = match reply { NativeConversion::Value(keys) => keys, NativeConversion::Throw(value) => { - return Ok(CopyStep::Complete(Completion::Throw(value))); + return Ok(CopyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let mut selected = Vec::new(); @@ -195,7 +197,9 @@ impl CopyResume { match read { crate::engine::object::OrdinaryRead::Complete(value) => { self.0.key = Some(key); - self.define_value(runtime, value.unwrap_or(Value::Undefined))?; + let value = runtime + .root_and_release_jsvalue(value.unwrap_or(JsValue::Undefined))?; + self.define_value(runtime, value)?; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( "object_copy_value_completed_locally", @@ -231,7 +235,7 @@ impl CopyResume { } }); } - Ok(CopyStep::Complete(Completion::Return(Value::Undefined))) + Ok(CopyStep::Complete(Completion::Return(JsValue::Undefined))) } fn define_value(&self, runtime: &Runtime, value: Value) -> Result<(), RuntimeError> { @@ -255,7 +259,9 @@ impl CopyResume { reply: NativeConversion, ) -> Result { match reply { - NativeConversion::Throw(value) => Ok(CopyStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(CopyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), NativeConversion::Value(false) => self.next(runtime), NativeConversion::Value(true) => Ok(CopyStep::Read { object: clone_copy_object(&self.0.source), @@ -275,7 +281,7 @@ impl CopyResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(CopyStep::Complete(Completion::Throw(value))), }; // Definition uses the unpublished target's own C_W_E data slot. @@ -297,9 +303,11 @@ pub(crate) fn finish( let PreparedCopyRead { read, key, resume } = *prepared; let completion = match runtime.finish_prepared_read(realm, &key, read)? { NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) + Completion::Return(runtime.into_jsvalue(value.unwrap_or(Value::Undefined))?) + } + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) } - NativeConversion::Throw(value) => Completion::Throw(value), }; resume.resume(runtime, completion)? } @@ -351,7 +359,7 @@ mod recovery_tests { assert_eq!(context.eval("copyTrace").unwrap(), Value::Int(0)); assert!(matches!( finish(&runtime, context.realm, step).unwrap(), - Completion::Return(Value::Undefined) + Completion::Return(JsValue::Undefined) )); assert_eq!(context.eval("copyTrace").unwrap(), Value::Int(1)); assert_eq!(runtime.own_property_keys(&target).unwrap().len(), 2); diff --git a/src/engine/builtins/object/definitions.rs b/src/engine/builtins/object/definitions.rs index 0c020088..26cfd042 100644 --- a/src/engine/builtins/object/definitions.rs +++ b/src/engine/builtins/object/definitions.rs @@ -7,7 +7,7 @@ use crate::engine::{ object::{ ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, operations::InternalDefineResult, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeArguments}, }; #[derive(Clone, Copy)] @@ -84,11 +84,14 @@ impl DefinitionsStep { ))?; let target = match kind { DefinitionsKind::Create => match value { - Value::Object(prototype) => runtime.new_object(Some(prototype))?, - Value::Null => runtime.new_object(None)?, + JsValue::Object(id) => { + let prototype = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; + runtime.new_object(Some(&prototype))? + } + JsValue::Null => runtime.new_object(None)?, _ => { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not a prototype", @@ -97,27 +100,38 @@ impl DefinitionsStep { } }, DefinitionsKind::Define => match value { - Value::Object(object) => object.clone(), + JsValue::Object(id) => ObjectRef::from_borrowed_handle(runtime.clone(), *id)?, _ => { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not an object", + )?, ))); } }, }; - let value = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Object definitions properties argv was not padded", - ))?; + let value = match arguments.readable.get(1) { + Some(value) => runtime.root_value(value)?, + None => { + return Err(RuntimeError::Invariant( + "Object definitions properties argv was not padded", + )); + } + }; if matches!(kind, DefinitionsKind::Create) && matches!(value, Value::Undefined) { - return Ok(Self::Complete(Completion::Return(Value::Object(target)))); + return Ok(Self::Complete(Completion::Return(JsValue::Object( + target.into_handle(), + )))); } let source = match runtime.native_to_object(realm, value)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; Ok(Self::request_keys( source.clone(), @@ -145,7 +159,9 @@ impl DefinitionsResume { let keys = match result { NativeConversion::Value(keys) => keys, NativeConversion::Throw(value) => { - return Ok(DefinitionsStep::Complete(Completion::Throw(value))); + return Ok(DefinitionsStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let mut selected = Vec::new(); @@ -194,7 +210,9 @@ impl DefinitionsResume { }; match result { NativeConversion::Throw(value) => { - return Ok(DefinitionsStep::Complete(Completion::Throw(value))); + return Ok(DefinitionsStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(true) => selected.push(key), NativeConversion::Value(false) => {} @@ -213,7 +231,7 @@ impl DefinitionsResume { ) -> Result { let Some(key) = remaining.next() else { return Ok(DefinitionsStep::Complete(Completion::Return( - Value::Object(self.0.target), + JsValue::Object(self.0.target.into_handle()), ))); }; Ok(DefinitionsStep::request_read( @@ -226,7 +244,11 @@ impl DefinitionsResume { }, )) } - pub(crate) fn read(mut self, result: Completion) -> Result { + pub(crate) fn read( + mut self, + _runtime: &Runtime, + result: Completion, + ) -> Result { let Phase::Read { remaining, key } = self.0.phase else { return Err(RuntimeError::Invariant( "Object definitions received unexpected value reply", @@ -243,6 +265,7 @@ impl DefinitionsResume { } pub(crate) fn converted( mut self, + runtime: &Runtime, result: NativeConversion, ) -> Result { let Phase::Convert { remaining, key } = self.0.phase else { @@ -251,7 +274,9 @@ impl DefinitionsResume { )); }; Ok(match result { - NativeConversion::Throw(value) => DefinitionsStep::Complete(Completion::Throw(value)), + NativeConversion::Throw(value) => { + DefinitionsStep::Complete(Completion::Throw(runtime.into_jsvalue(value)?)) + } NativeConversion::Value(descriptor) => { DefinitionsStep::request_define(self.0.target.clone(), key.clone(), descriptor, { let updated_0 = Phase::Define { remaining, key }; @@ -272,7 +297,9 @@ impl DefinitionsResume { )); }; if let Some(value) = runtime.finish_define_property_or_throw(self.0.realm, &key, result)? { - return Ok(DefinitionsStep::Complete(Completion::Throw(value))); + return Ok(DefinitionsStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } { let updated_0 = Phase::Keys; @@ -305,11 +332,17 @@ pub(super) fn finish( DefinitionsStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - resume.read(runtime.get_property_in_realm(realm, &object, &key)?)? + resume.read( + runtime, + runtime.get_property_in_realm(realm, &object, &key)?, + )? } DefinitionsStep::Convert { mut resume } => { - let value = resume.take_convert_value(); - resume.converted(runtime.native_to_property_descriptor(realm, value)?)? + let value = runtime.root_and_release_jsvalue(resume.take_convert_value())?; + resume.converted( + runtime, + runtime.native_to_property_descriptor(realm, value)?, + )? } DefinitionsStep::Define { mut resume } => { let object = resume.take_define_object(); @@ -331,7 +364,7 @@ struct DefinitionsStepPending { enumerable_key: Option, read_object: Option, read_key: Option, - convert_value: Option, + convert_value: Option, define_object: Option, define_key: Option, define_descriptor: Option, @@ -359,7 +392,7 @@ impl DefinitionsStep { resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_convert(value: Value, mut resume: DefinitionsResume) -> Self { + pub(crate) fn request_convert(value: JsValue, mut resume: DefinitionsResume) -> Self { resume.0.pending_effect.convert_value = Some(value); Self::Convert { resume } } @@ -411,7 +444,7 @@ impl DefinitionsResume { .take() .expect("DefinitionsStep Read key") } - pub(crate) fn take_convert_value(&mut self) -> Value { + pub(crate) fn take_convert_value(&mut self) -> JsValue { self.0 .pending_effect .convert_value diff --git a/src/engine/builtins/object/iteration.rs b/src/engine/builtins/object/iteration.rs index ca7b1d2e..360b7120 100644 --- a/src/engine/builtins/object/iteration.rs +++ b/src/engine/builtins/object/iteration.rs @@ -11,7 +11,7 @@ use crate::engine::{ CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, operations::InternalDefineResult, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -75,6 +75,7 @@ impl std::ops::DerefMut for IterationResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct IterationResumeState { + runtime: Runtime, pending_effect: IterationStepPending, realm: ContextId, kind: IterationKind, @@ -86,6 +87,33 @@ pub(crate) struct IterationResumeState { limit: u64, phase: Phase, } +impl Drop for IterationResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(value) = self.pending_effect.next_method.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.key_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.pending_effect.push_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} enum Phase { IteratorMethod(Value), Iterator, @@ -135,13 +163,18 @@ impl IterationStep { let value = arguments.readable.get(1).ok_or(RuntimeError::Invariant( "groupBy callback argv was not padded", ))?; + let value = runtime.root_value(value)?; let callback = match value { - Value::Object(object) => runtime.as_callable(object)?, + Value::Object(object) => runtime.as_callable(&object)?, _ => None, }; let Some(callback) = callback else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; Some(callback) @@ -153,13 +186,9 @@ impl IterationStep { } else { None }; - let iterable = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Object iterator argv was not padded", - ))?; + let iterable = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Object iterator argv was not padded"), + )?)?; if matches!(iterable, Value::Null | Value::Undefined) { let base = if matches!(iterable, Value::Null) { "null" @@ -167,7 +196,7 @@ impl IterationStep { "undefined" }; return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!("cannot read property 'Symbol.iterator' of {base}"), @@ -175,9 +204,10 @@ impl IterationStep { ))); } Ok(Self::request_read( - iterable.clone(), + runtime.into_jsvalue(iterable.clone())?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), IterationResume(Box::new(IterationResumeState { + runtime: runtime.clone(), pending_effect: IterationStepPending::default(), realm, kind, @@ -204,10 +234,10 @@ impl IterationResume { "Object iterator result not allocated", )) } - fn abrupt(self, value: Value) -> IterationStep { + fn abrupt(mut self, value: JsValue) -> IterationStep { let close = matches!(self.0.kind, IterationKind::Entries) || matches!(self.0.phase, Phase::Callback(_) | Phase::Key(_)); - if close && let Some(iterator) = self.0.iterator { + if close && let Some(iterator) = self.0.iterator.take() { IterationStep::Close { iterator, completion: Completion::Throw(value), @@ -220,7 +250,7 @@ impl IterationResume { if !matches!(self.0.kind, IterationKind::Entries) && self.0.index >= self.0.limit { return Ok(IterationStep::Close { iterator: self.iterator()?, - completion: Completion::Throw(runtime.new_native_error( + completion: Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "too many elements", @@ -230,7 +260,7 @@ impl IterationResume { self.0.phase = Phase::Next; Ok(IterationStep::request_next( self.iterator()?, - self.0.next.clone(), + runtime.into_jsvalue(self.0.next.clone())?, self, )) } @@ -245,18 +275,21 @@ impl IterationResume { )); } let value = match reply { - ObjectIteratorStep::Throw(value) => return Ok(self.abrupt(value)), + ObjectIteratorStep::Throw(value) => { + return Ok(self.abrupt(value)); + } ObjectIteratorStep::Done => { - return Ok(IterationStep::Complete(Completion::Return(Value::Object( - self.result()?, - )))); + let result = self.result()?; + return Ok(IterationStep::Complete(Completion::Return( + JsValue::Object(result.into_handle()), + ))); } - ObjectIteratorStep::Yield(value) => value, + ObjectIteratorStep::Yield(value) => runtime.root_and_release_jsvalue(value)?, }; match self.0.kind { IterationKind::Entries => { let Value::Object(item) = value else { - let value = runtime.new_native_error( + let value = runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", @@ -265,7 +298,7 @@ impl IterationResume { }; self.0.phase = Phase::EntryKey(item.clone()); Ok(IterationStep::request_read( - Value::Object(item), + JsValue::Object(item.into_handle()), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Literal1)?, self, @@ -277,11 +310,14 @@ impl IterationResume { .callback .clone() .ok_or(RuntimeError::Invariant("groupBy callback missing"))?; - let arguments = vec![value.clone(), Value::number(self.0.index as f64)]; + let arguments = vec![ + runtime.into_jsvalue(value.clone())?, + runtime.into_jsvalue(Value::number(self.0.index as f64))?, + ]; self.0.phase = Phase::Callback(value); Ok(IterationStep::request_call( callable, - Value::Object(runtime.global_object_for_realm(self.0.realm)?), + JsValue::Object(runtime.global_object_for_realm(self.0.realm)?.into_handle()), arguments, self, )) @@ -295,7 +331,7 @@ impl IterationResume { ) -> Result { let value = match reply { Completion::Throw(value) => return Ok(self.abrupt(value)), - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, }; let phase = std::mem::replace(&mut self.0.phase, Phase::Next); match phase { @@ -306,7 +342,7 @@ impl IterationResume { }; let Some(callable) = callable else { return Ok(IterationStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "value is not iterable", @@ -316,7 +352,7 @@ impl IterationResume { self.0.phase = Phase::Iterator; Ok(IterationStep::request_call( callable, - iterable, + runtime.into_jsvalue(iterable)?, Vec::new(), self, )) @@ -324,7 +360,7 @@ impl IterationResume { Phase::Iterator => { let Value::Object(iterator) = value else { return Ok(IterationStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not an object", @@ -334,7 +370,7 @@ impl IterationResume { self.0.iterator = Some(iterator.clone()); self.0.phase = Phase::NextMethod; Ok(IterationStep::request_read( - Value::Object(iterator), + JsValue::Object(iterator.into_handle()), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?, self, )) @@ -353,7 +389,7 @@ impl IterationResume { Phase::EntryKey(item) => { self.0.phase = Phase::EntryValue(value); Ok(IterationStep::request_read( - Value::Object(item), + JsValue::Object(item.into_handle()), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Literal2)?, self, @@ -361,11 +397,11 @@ impl IterationResume { } Phase::EntryValue(key) => { self.0.phase = Phase::Key(value); - Ok(IterationStep::request_key(key, self)) + Ok(IterationStep::request_key(runtime.into_jsvalue(key)?, self)) } Phase::Callback(item) => { if matches!(self.0.kind, IterationKind::MapGroup) { - let key = Runtime::normalized_map_key(value); + let key = Runtime::normalized_map_key(runtime.into_jsvalue(value)?); let groups = self.result()?; let group = match runtime.find_map_record(&groups, &key)? { Some((_, value)) => match runtime.root_raw_value(&value)? { @@ -378,15 +414,26 @@ impl IterationResume { }, None => { let group = runtime.new_array(self.0.realm)?; - runtime.set_map_record(&groups, key, Value::Object(group.clone()))?; + runtime.set_map_record( + &groups, + key, + runtime.into_jsvalue(Value::Object(group.clone()))?, + )?; group } }; self.0.phase = Phase::Push; - return Ok(IterationStep::request_push(group, item, self)); + return Ok(IterationStep::request_push( + group, + runtime.into_jsvalue(item)?, + self, + )); } self.0.phase = Phase::Key(item); - Ok(IterationStep::request_key(value, self)) + Ok(IterationStep::request_key( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Group(item, key) => { let group = match value { @@ -408,7 +455,11 @@ impl IterationResume { } }; self.0.phase = Phase::Push; - Ok(IterationStep::request_push(group, item, self)) + Ok(IterationStep::request_push( + group, + runtime.into_jsvalue(item)?, + self, + )) } Phase::Push => { self.0.index = self.0.index.checked_add(1).ok_or(RuntimeError::Invariant( @@ -428,11 +479,13 @@ impl IterationResume { ) -> Result { let value = match reply { Completion::Throw(value) => return Ok(self.abrupt(value)), - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, }; let key = match runtime.property_key_from_primitive(self.0.realm, value)? { NativeConversion::Value(key) => key, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); + } }; let Phase::Key(item) = std::mem::replace(&mut self.0.phase, Phase::Next) else { return Err(RuntimeError::Invariant( @@ -452,7 +505,7 @@ impl IterationResume { IterationKind::Group => { self.0.phase = Phase::Group(item, key.clone()); Ok(IterationStep::request_read( - Value::Object(self.result()?), + JsValue::Object(self.result()?.into_handle()), key, self, )) @@ -468,7 +521,9 @@ impl IterationResume { reply: NativeConversion, ) -> Result { let result = match reply { - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); + } NativeConversion::Value(result) => result, }; match std::mem::replace(&mut self.0.phase, Phase::Next) { @@ -478,7 +533,7 @@ impl IterationResume { &key, NativeConversion::Value(result), )? { - return Ok(self.abrupt(value)); + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); } self.next_step(runtime) } @@ -489,7 +544,11 @@ impl IterationResume { )); } self.0.phase = Phase::Push; - Ok(IterationStep::request_push(group, value, self)) + Ok(IterationStep::request_push( + group, + runtime.into_jsvalue(value)?, + self, + )) } _ => Err(RuntimeError::Invariant( "Object iterator definition has wrong phase", @@ -525,7 +584,7 @@ pub(crate) fn finish( ); } IterationStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -534,8 +593,12 @@ pub(crate) fn finish( } IterationStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -543,7 +606,7 @@ pub(crate) fn finish( } IterationStep::Next { mut resume } => { let iterator = resume.take_next_iterator(); - let method = resume.take_next_method(); + let method = runtime.root_and_release_jsvalue(resume.take_next_method())?; resume.next( runtime, finish_next( @@ -556,7 +619,7 @@ pub(crate) fn finish( IterationStep::Key { mut resume } => { let value = resume.take_key_value(); { - let result = runtime.to_primitive( + let result = runtime.to_primitive_jsvalue( realm, value, crate::engine::vm::ToPrimitiveHint::String, @@ -582,7 +645,7 @@ pub(crate) fn finish( realm, ArrayPushKind::Push, NativeInvocation::Call { - this_value: Value::Object(object), + this_value: runtime.into_jsvalue(Value::Object(object))?, }, &NativeArguments { actual_arg_count: 1, @@ -606,25 +669,29 @@ mod tests { let iterable_id = iterable.object_id(); let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::Object(iterable)], + readable: vec![runtime.into_jsvalue(Value::Object(iterable)).unwrap()], }; let IterationStep::Read { mut resume } = IterationStep::start( &runtime, context.realm, IterationKind::Entries, &NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &arguments, ) .unwrap() else { panic!("iterator method read expected") }; - let _ = resume.take_read_receiver(); + runtime + .release_jsvalue(resume.take_read_receiver()) + .unwrap(); let _ = resume.take_read_key(); let result_id = resume.result.as_ref().unwrap().object_id(); - drop(arguments); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } runtime.run_gc().unwrap(); for id in [iterable_id, result_id] { assert!(runtime.0.state.borrow().heap.object(id).is_ok()); @@ -639,23 +706,23 @@ mod tests { #[derive(Default)] struct IterationStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, next_iterator: Option, - next_method: Option, - key_value: Option, + next_method: Option, + key_value: Option, define_object: Option, define_key: Option, define_descriptor: Option, push_object: Option, - push_value: Option, + push_value: Option, } impl IterationStep { pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: IterationResume, ) -> Self { @@ -665,8 +732,8 @@ impl IterationStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: IterationResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -676,14 +743,14 @@ impl IterationStep { } pub(crate) fn request_next( iterator: ObjectRef, - method: Value, + method: JsValue, mut resume: IterationResume, ) -> Self { resume.0.pending_effect.next_iterator = Some(iterator); resume.0.pending_effect.next_method = Some(method); Self::Next { resume } } - pub(crate) fn request_key(value: Value, mut resume: IterationResume) -> Self { + pub(crate) fn request_key(value: JsValue, mut resume: IterationResume) -> Self { resume.0.pending_effect.key_value = Some(value); Self::Key { resume } } @@ -700,7 +767,7 @@ impl IterationStep { } pub(crate) fn request_push( object: ObjectRef, - value: Value, + value: JsValue, mut resume: IterationResume, ) -> Self { resume.0.pending_effect.push_object = Some(object); @@ -709,7 +776,7 @@ impl IterationStep { } } impl IterationResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -730,14 +797,14 @@ impl IterationResume { .take() .expect("IterationStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("IterationStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -751,14 +818,14 @@ impl IterationResume { .take() .expect("IterationStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.0 .pending_effect .next_method .take() .expect("IterationStep Next method") } - pub(crate) fn take_key_value(&mut self) -> Value { + pub(crate) fn take_key_value(&mut self) -> JsValue { self.0 .pending_effect .key_value @@ -793,7 +860,7 @@ impl IterationResume { .take() .expect("IterationStep Push object") } - pub(crate) fn take_push_value(&mut self) -> Value { + pub(crate) fn take_push_value(&mut self) -> JsValue { self.0 .pending_effect .push_value diff --git a/src/engine/builtins/object/predicate.rs b/src/engine/builtins/object/predicate.rs index 3fe0a985..d5c6c7a9 100644 --- a/src/engine/builtins/object/predicate.rs +++ b/src/engine/builtins/object/predicate.rs @@ -10,7 +10,7 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -82,16 +82,20 @@ impl PredicateStep { "own predicate did not receive a call", )); }; + let this_value = runtime.root_value(this_value)?; let (receiver, key) = if matches!(kind, PredicateKind::HasOwn) { - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant("hasOwn target argv was not padded"))?; + let value = match arguments.readable.first() { + Some(value) => runtime.root_value(value)?, + None => { + return Err(RuntimeError::Invariant("hasOwn target argv was not padded")); + } + }; let object = match runtime.native_to_object(realm, value)? { NativeConversion::Value(object) => object, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; (Value::Object(object), arguments.readable.get(1)) @@ -99,7 +103,9 @@ impl PredicateStep { let object = match runtime.native_to_object(realm, this_value.clone())? { NativeConversion::Value(object) => object, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; (Value::Object(object), arguments.readable.first()) @@ -112,12 +118,18 @@ impl PredicateStep { .get(1) .ok_or(RuntimeError::Invariant("accessor argv was not padded"))?; let callable = match value { - Value::Object(object) => runtime.as_callable(object)?, + JsValue::Object(id) => { + let object = crate::engine::object::ObjectRef::from_borrowed_handle( + runtime.clone(), + *id, + )?; + runtime.as_callable(&object)? + } _ => None, }; let Some(callable) = callable else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, crate::engine::api::error::NativeErrorKind::Type, "not a function", @@ -128,9 +140,14 @@ impl PredicateStep { } else { None }; - let value = key.cloned().ok_or(RuntimeError::Invariant( - "own predicate key argv was not padded", - ))?; + let value = match key { + Some(key) => runtime.dup_jsvalue(key)?, + None => { + return Err(RuntimeError::Invariant( + "own predicate key argv was not padded", + )); + } + }; Ok(Self::request_key( value, PredicateResume(Box::new(PredicateResumeState { @@ -155,7 +172,7 @@ impl PredicateResume { )); }; let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(PredicateStep::Complete(Completion::Throw(value))); } @@ -163,13 +180,17 @@ impl PredicateResume { let key = match runtime.property_key_from_primitive(self.0.realm, value)? { NativeConversion::Value(key) => key, NativeConversion::Throw(value) => { - return Ok(PredicateStep::Complete(Completion::Throw(value))); + return Ok(PredicateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let object = match runtime.native_to_object(self.0.realm, self.0.receiver.clone())? { NativeConversion::Value(object) => object, NativeConversion::Throw(value) => { - return Ok(PredicateStep::Complete(Completion::Throw(value))); + return Ok(PredicateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let resume = { @@ -208,6 +229,7 @@ impl PredicateResume { } pub(crate) fn boolean( self, + runtime: &Runtime, result: NativeConversion, ) -> Result { if !matches!(self.0.phase, Phase::Own(_)) @@ -221,8 +243,8 @@ impl PredicateResume { )); } Ok(PredicateStep::Complete(match result { - NativeConversion::Value(value) => Completion::Return(Value::Bool(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => Completion::Return(JsValue::Bool(value)), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), })) } } @@ -244,13 +266,14 @@ impl PredicateResume { } Ok(PredicateStep::Complete( match runtime.finish_define_property_or_throw(self.0.realm, &key, result)? { - Some(value) => Completion::Throw(value), - None => Completion::Return(Value::Undefined), + Some(value) => Completion::Throw(runtime.into_jsvalue(value)?), + None => Completion::Return(JsValue::Undefined), }, )) } pub(crate) fn descriptor( mut self, + runtime: &Runtime, result: NativeConversion>, ) -> Result { let Phase::Own(key) = self.0.phase else { @@ -262,7 +285,9 @@ impl PredicateResume { let descriptor = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(PredicateStep::Complete(Completion::Throw(value))); + return Ok(PredicateStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Some(descriptor) = descriptor else { @@ -285,10 +310,13 @@ impl PredicateResume { Value::Object(callable.into_object()) }), }; - Ok(PredicateStep::Complete(Completion::Return(value))) + Ok(PredicateStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))) } pub(crate) fn prototype( mut self, + runtime: &Runtime, result: NativeConversion>, ) -> Result { let Phase::Prototype(key) = self.0.phase else { @@ -297,9 +325,11 @@ impl PredicateResume { )); }; Ok(match result { - NativeConversion::Throw(value) => PredicateStep::Complete(Completion::Throw(value)), + NativeConversion::Throw(value) => { + PredicateStep::Complete(Completion::Throw(runtime.into_jsvalue(value)?)) + } NativeConversion::Value(None) => { - PredicateStep::Complete(Completion::Return(Value::Undefined)) + PredicateStep::Complete(Completion::Return(JsValue::Undefined)) } NativeConversion::Value(Some(object)) => { PredicateStep::request_descriptor(object.clone(), key.clone(), { @@ -325,11 +355,14 @@ pub(in crate::engine::builtins) fn finish( PredicateStep::Descriptor { mut resume } => { let object = resume.take_descriptor_object(); let key = resume.take_descriptor_key(); - resume.descriptor(runtime.internal_get_own_property(realm, &object, &key)?)? + resume.descriptor( + runtime, + runtime.internal_get_own_property(realm, &object, &key)?, + )? } PredicateStep::Prototype { mut resume } => { let object = resume.take_prototype_object(); - resume.prototype(runtime.internal_get_prototype_of(realm, &object)?)? + resume.prototype(runtime, runtime.internal_get_prototype_of(realm, &object)?)? } PredicateStep::Define { mut resume } => { let object = resume.take_define_object(); @@ -341,7 +374,7 @@ pub(in crate::engine::builtins) fn finish( )? } PredicateStep::Key { mut resume } => { - let value = resume.take_key_value(); + let value = runtime.root_and_release_jsvalue(resume.take_key_value())?; resume.key( runtime, runtime.to_primitive(realm, value, ToPrimitiveHint::String)?, @@ -351,11 +384,14 @@ pub(in crate::engine::builtins) fn finish( let object = resume.take_own_object(); let key = resume.take_own_key(); let enumerable = resume.take_own_enumerable(); - resume.boolean(if enumerable { - runtime.internal_own_property_is_enumerable(realm, &object, &key)? - } else { - runtime.internal_has_own_property(realm, &object, &key)? - })? + resume.boolean( + runtime, + if enumerable { + runtime.internal_own_property_is_enumerable(realm, &object, &key)? + } else { + runtime.internal_has_own_property(realm, &object, &key)? + }, + )? } }; } @@ -369,7 +405,7 @@ struct PredicateStepPending { define_object: Option, define_key: Option, define_descriptor: Option, - key_value: Option, + key_value: Option, own_object: Option, own_key: Option, own_enumerable: Option, @@ -399,7 +435,7 @@ impl PredicateStep { resume.0.pending_effect.define_descriptor = Some(descriptor); Self::Define { resume } } - pub(crate) fn request_key(value: Value, mut resume: PredicateResume) -> Self { + pub(crate) fn request_key(value: JsValue, mut resume: PredicateResume) -> Self { resume.0.pending_effect.key_value = Some(value); Self::Key { resume } } @@ -458,7 +494,7 @@ impl PredicateResume { .take() .expect("PredicateStep Define descriptor") } - pub(crate) fn take_key_value(&mut self) -> Value { + pub(crate) fn take_key_value(&mut self) -> JsValue { self.0 .pending_effect .key_value diff --git a/src/engine/builtins/object/property.rs b/src/engine/builtins/object/property.rs index 04a14733..3f0a1a3c 100644 --- a/src/engine/builtins/object/property.rs +++ b/src/engine/builtins/object/property.rs @@ -12,10 +12,15 @@ use crate::engine::{ object::{ CompleteOrdinaryPropertyDescriptor, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeArguments}, }; +/// Duplicate an owned object root into an independent internal-value edge. +fn js_object_value(runtime: &Runtime, object: &ObjectRef) -> Result { + runtime.dup_jsvalue(&JsValue::Object(object.object_id())) +} + #[derive(Clone, Copy)] pub(crate) enum PropertyKind { Integrity(ObjectIntegrityKind), @@ -105,8 +110,8 @@ pub(crate) struct PropertyResumeState { } enum Phase { Key { - value: Value, - receiver: Value, + value: JsValue, + receiver: JsValue, }, Descriptor(PropertyKey), Defined(PropertyKey), @@ -120,7 +125,7 @@ enum Phase { key: PropertyKey, }, AssignKeys { - sources: std::vec::IntoIter, + sources: std::vec::IntoIter, source: ObjectRef, snapshot: bool, }, @@ -146,7 +151,7 @@ enum Phase { }, } struct Assignment { - sources: std::vec::IntoIter, + sources: std::vec::IntoIter, source: ObjectRef, remaining: std::vec::IntoIter, snapshot: bool, @@ -163,28 +168,28 @@ impl PropertyStep { kind: PropertyKind, arguments: &NativeArguments, ) -> Result { - let target = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "property builtin argv was not padded", - ))?; + let target = arguments.readable.first().ok_or(RuntimeError::Invariant( + "property builtin argv was not padded", + ))?; let object = match (kind, target) { - (_, Value::Object(object)) => object, + (_, JsValue::Object(id)) => ObjectRef::from_borrowed_handle(runtime.clone(), *id)?, (PropertyKind::Integrity(kind), value) => { return Ok(Self::Complete(Completion::Return(match kind { - ObjectIntegrityKind::Seal | ObjectIntegrityKind::Freeze => value, + ObjectIntegrityKind::Seal | ObjectIntegrityKind::Freeze => { + runtime.dup_jsvalue(value)? + } ObjectIntegrityKind::IsSealed | ObjectIntegrityKind::IsFrozen => { - Value::Bool(true) + JsValue::Bool(true) } }))); } (PropertyKind::ObjectExtensible, _) => { - return Ok(Self::Complete(Completion::Return(Value::Bool(false)))); + return Ok(Self::Complete(Completion::Return(JsValue::Bool(false)))); } (PropertyKind::ObjectPrevent, value) => { - return Ok(Self::Complete(Completion::Return(value))); + return Ok(Self::Complete(Completion::Return( + runtime.dup_jsvalue(value)?, + ))); } ( PropertyKind::Assign @@ -193,20 +198,26 @@ impl PropertyStep { | PropertyKind::ObjectOwnKeys(_) | PropertyKind::ObjectDescriptors, value, - ) => match runtime.native_to_object(realm, value)? { + ) => match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(value)?)? { NativeConversion::Value(object) => object, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }, _ => { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not an object", + )?, ))); } }; let resume = PropertyResume(Box::new(PropertyResumeState { - pending_effect: PropertyStepPending::default(), + pending_effect: PropertyStepPending::new(runtime.clone()), realm, kind, object: object.clone(), @@ -223,7 +234,9 @@ impl PropertyStep { sources.try_reserve_exact(count).map_err(|_| { RuntimeError::Invariant("Object.assign sources allocation failed") })?; - sources.extend(arguments.readable.iter().skip(1).take(count).cloned()); + for value in arguments.readable.iter().skip(1).take(count) { + sources.push(runtime.dup_jsvalue(value)?); + } resume.assign_source(runtime, sources.into_iter()) } PropertyKind::Keys @@ -237,29 +250,24 @@ impl PropertyStep { Ok(Self::request_prevent(object, resume)) } _ => { - let key = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "property builtin key argv was not padded", - ))?; + let raw_key = arguments.readable.get(1).ok_or(RuntimeError::Invariant( + "property builtin key argv was not padded", + ))?; let receiver_index = if matches!(kind, PropertyKind::Get) { 2 } else { 3 }; let receiver = if arguments.actual_arg_count > receiver_index { - arguments.readable[receiver_index].clone() + runtime.dup_jsvalue(&arguments.readable[receiver_index])? } else { - Value::Object(object) + js_object_value(runtime, &object)? + }; + let value = match arguments.readable.get(2) { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, }; - let value = arguments - .readable - .get(2) - .cloned() - .unwrap_or(Value::Undefined); - Ok(Self::request_key(key, { + Ok(Self::request_key(runtime.dup_jsvalue(raw_key)?, { let updated = Phase::Key { value, receiver }; let mut resident = resume; resident.0.phase = updated; @@ -270,6 +278,16 @@ impl PropertyStep { } } impl PropertyResume { + /// Release the owned edges of an abandoned pre-key phase. Releases are + /// defer-safe and nothrow, matching the pending-effect cleanup contract. + fn release_key_phase(&mut self, runtime: &Runtime) { + let Phase::Key { value, receiver } = std::mem::replace(&mut self.0.phase, Phase::Result) + else { + return; + }; + let _ = runtime.release_jsvalue(value); + let _ = runtime.release_jsvalue(receiver); + } pub(crate) fn keys( mut self, runtime: &Runtime, @@ -280,7 +298,9 @@ impl PropertyResume { } let keys = match result { NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(keys) => keys, }; @@ -313,7 +333,9 @@ impl PropertyResume { &key, )? { NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(false) => continue, NativeConversion::Value(true) => {} @@ -361,11 +383,14 @@ impl PropertyResume { _ => unreachable!(), }; if include { - values.push(runtime.object_property_key_value(&key)?); + values + .push(runtime.into_jsvalue(runtime.object_property_key_value(&key)?)?); } } - Ok(PropertyStep::Complete(Completion::Return(Value::Object( - runtime.new_array_from_values(self.0.realm, values)?, + Ok(PropertyStep::Complete(Completion::Return(JsValue::Object( + runtime + .new_array_from_values_jsvalue(self.0.realm, values)? + .into_handle(), )))) } PropertyKind::ObjectKeys(_) | PropertyKind::ObjectDescriptors => { @@ -415,8 +440,8 @@ impl PropertyResume { self.0.kind, PropertyKind::Integrity(ObjectIntegrityKind::Seal | ObjectIntegrityKind::Freeze) ) { - Ok(PropertyStep::Complete(Completion::Return(Value::Object( - self.0.object, + Ok(PropertyStep::Complete(Completion::Return(JsValue::Object( + self.0.object.into_handle(), )))) } else { Ok(PropertyStep::request_extensible(self.0.object.clone(), { @@ -429,13 +454,13 @@ impl PropertyResume { fn assign_source( mut self, runtime: &Runtime, - mut sources: std::vec::IntoIter, + mut sources: std::vec::IntoIter, ) -> Result { for value in sources.by_ref() { - if matches!(value, Value::Null | Value::Undefined) { + if matches!(value, JsValue::Null | JsValue::Undefined) { continue; } - let source = match runtime.native_to_object(self.0.realm, value)? { + let source = match runtime.native_to_object_jsvalue(self.0.realm, value)? { NativeConversion::Value(source) => source, NativeConversion::Throw(_) => { return Err(RuntimeError::Invariant( @@ -454,8 +479,8 @@ impl PropertyResume { self })); } - Ok(PropertyStep::Complete(Completion::Return(Value::Object( - self.0.object, + Ok(PropertyStep::Complete(Completion::Return(JsValue::Object( + self.0.object.into_handle(), )))) } fn assign_next( @@ -467,7 +492,7 @@ impl PropertyResume { return self.assign_source(runtime, state.sources); }; if state.snapshot { - return self.assign_read(state, key); + return self.assign_read(runtime, state, key); } Ok(PropertyStep::request_descriptor( state.source.clone(), @@ -481,13 +506,14 @@ impl PropertyResume { } fn assign_read( mut self, + runtime: &Runtime, state: Assignment, key: PropertyKey, ) -> Result { Ok(PropertyStep::request_read( state.source.clone(), key.clone(), - Value::Object(state.source.clone()), + js_object_value(runtime, &state.source)?, { let updated_0 = Phase::AssignRead { state, key }; self.0.phase = updated_0; @@ -522,20 +548,21 @@ impl PropertyResume { }, )); } - Ok(PropertyStep::Complete(Completion::Return(Value::Object( - state.result, + Ok(PropertyStep::Complete(Completion::Return(JsValue::Object( + state.result.into_handle(), )))) } fn emit( self, runtime: &Runtime, mut state: Enumeration, - value: Value, + value: JsValue, ) -> Result { + let element = runtime.root_and_release_jsvalue(value)?; runtime.define_fresh_object_keys_array_element( &state.result, state.index, - value, + element, "fresh Object keys result rejected an element", )?; state.index = state.index.checked_add(1).ok_or_else(|| { @@ -554,13 +581,21 @@ impl PropertyResume { let value = match result { Completion::Return(value) => value, Completion::Throw(value) => { + self.release_key_phase(runtime); return Ok(PropertyStep::Complete(Completion::Throw(value))); } }; - let key = match runtime.property_key_from_primitive(self.0.realm, value)? { - NativeConversion::Value(key) => key, - NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + let key = match runtime.property_key_from_primitive_jsvalue(self.0.realm, value) { + Ok(NativeConversion::Value(key)) => key, + Ok(NativeConversion::Throw(value)) => { + self.release_key_phase(runtime); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + Err(error) => { + self.release_key_phase(runtime); + return Err(error); } }; let Phase::Key { value, receiver } = self.0.phase else { @@ -575,14 +610,25 @@ impl PropertyResume { self }; Ok(match resume.kind { - PropertyKind::Get => PropertyStep::request_read(object, key, receiver, resume), + PropertyKind::Get => { + let _ = runtime.release_jsvalue(value); + PropertyStep::request_read(object, key, receiver, resume) + } PropertyKind::Set => PropertyStep::request_set(object, key, value, receiver, resume), - PropertyKind::Has => PropertyStep::request_has(object, key, resume), - PropertyKind::Delete => PropertyStep::request_delete(object, key, resume), - PropertyKind::Descriptor | PropertyKind::ObjectDescriptor => { - PropertyStep::request_descriptor(object, key, resume) + PropertyKind::Has + | PropertyKind::Delete + | PropertyKind::Descriptor + | PropertyKind::ObjectDescriptor => { + let _ = runtime.release_jsvalue(value); + let _ = runtime.release_jsvalue(receiver); + match resume.kind { + PropertyKind::Has => PropertyStep::request_has(object, key, resume), + PropertyKind::Delete => PropertyStep::request_delete(object, key, resume), + _ => PropertyStep::request_descriptor(object, key, resume), + } } PropertyKind::Define | PropertyKind::ObjectDefine => { + let _ = runtime.release_jsvalue(receiver); PropertyStep::request_convert(value, { let updated = Phase::Descriptor(key); let mut resident = resume; @@ -591,6 +637,8 @@ impl PropertyResume { }) } _ => { + let _ = runtime.release_jsvalue(value); + let _ = runtime.release_jsvalue(receiver); return Err(RuntimeError::Invariant( "property builtin does not accept a key", )); @@ -599,6 +647,7 @@ impl PropertyResume { } pub(crate) fn converted( mut self, + runtime: &Runtime, result: NativeConversion, ) -> Result { let Phase::Descriptor(key) = self.0.phase else { @@ -607,7 +656,9 @@ impl PropertyResume { )); }; Ok(match result { - NativeConversion::Throw(value) => PropertyStep::Complete(Completion::Throw(value)), + NativeConversion::Throw(value) => { + PropertyStep::Complete(Completion::Throw(runtime.into_jsvalue(value)?)) + } NativeConversion::Value(descriptor) => { PropertyStep::request_define(self.0.object.clone(), key.clone(), descriptor, { let updated_0 = Phase::Defined(key); @@ -626,7 +677,9 @@ impl PropertyResume { if let Some(value) = runtime.finish_define_property_or_throw(self.0.realm, &key, result)? { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } return { let updated_0 = Phase::Result; @@ -643,16 +696,18 @@ impl PropertyResume { Ok(PropertyStep::Complete( if matches!(self.0.kind, PropertyKind::ObjectDefine) { match runtime.finish_define_property_or_throw(self.0.realm, &key, result)? { - Some(value) => Completion::Throw(value), - None => Completion::Return(Value::Object(self.0.object)), + Some(value) => Completion::Throw(runtime.into_jsvalue(value)?), + None => Completion::Return(JsValue::Object(self.0.object.into_handle())), } } else { match result { - NativeConversion::Value(result) => Completion::Return(Value::Bool(matches!( + NativeConversion::Value(result) => Completion::Return(JsValue::Bool(matches!( result, InternalDefineResult::Defined ))), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) + } } }, )) @@ -666,7 +721,9 @@ impl PropertyResume { let current = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let PropertyKind::Integrity(kind) = self.0.kind else { @@ -689,7 +746,7 @@ impl PropertyResume { } }); return if violates { - Ok(PropertyStep::Complete(Completion::Return(Value::Bool( + Ok(PropertyStep::Complete(Completion::Return(JsValue::Bool( false, )))) } else { @@ -730,7 +787,9 @@ impl PropertyResume { descriptor.is_some_and(|descriptor| descriptor.enumerable()) } NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let resume = { @@ -739,7 +798,7 @@ impl PropertyResume { self }; return if enumerable { - resume.assign_read(state, key) + resume.assign_read(runtime, state, key) } else { resume.assign_next(runtime, state) }; @@ -752,7 +811,9 @@ impl PropertyResume { }; let descriptor = match result { NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(None) => return resume.enumerate(runtime, state), NativeConversion::Value(Some(descriptor)) => descriptor, @@ -772,7 +833,11 @@ impl PropertyResume { return resume.enumerate(runtime, state); } if matches!(resume.kind, PropertyKind::ObjectKeys(ObjectKeysKind::Keys)) { - return resume.emit(runtime, state, runtime.object_property_key_value(&key)?); + return resume.emit( + runtime, + state, + runtime.into_jsvalue(runtime.object_property_key_value(&key)?)?, + ); } let pair = if matches!( resume.kind, @@ -792,7 +857,7 @@ impl PropertyResume { return Ok(PropertyStep::request_read( resume.object.clone(), key, - Value::Object(resume.object.clone()), + js_object_value(runtime, &resume.object)?, { let updated = Phase::Entry { state, pair }; let mut resident = resume; @@ -811,10 +876,12 @@ impl PropertyResume { )); } Ok(PropertyStep::Complete(match result { - NativeConversion::Throw(value) => Completion::Throw(value), - NativeConversion::Value(None) => Completion::Return(Value::Undefined), - NativeConversion::Value(Some(descriptor)) => Completion::Return(Value::Object( - runtime.complete_descriptor_to_object(self.0.realm, descriptor)?, + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), + NativeConversion::Value(None) => Completion::Return(JsValue::Undefined), + NativeConversion::Value(Some(descriptor)) => Completion::Return(JsValue::Object( + runtime + .complete_descriptor_to_object(self.0.realm, descriptor)? + .into_handle(), )), })) } @@ -832,19 +899,21 @@ impl PropertyResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; return if matches!( kind, ObjectIntegrityKind::IsSealed | ObjectIntegrityKind::IsFrozen ) { - Ok(PropertyStep::Complete(Completion::Return(Value::Bool( + Ok(PropertyStep::Complete(Completion::Return(JsValue::Bool( !value, )))) } else if !value { Ok(PropertyStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "proxy preventExtensions handler returned false", @@ -869,21 +938,21 @@ impl PropertyResume { return Err(RuntimeError::Invariant("boolean reply has wrong phase")); } Ok(PropertyStep::Complete(match result { - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), NativeConversion::Value(accepted) if matches!(self.0.kind, PropertyKind::ObjectPrevent) => { if accepted { - Completion::Return(Value::Object(self.0.object)) + Completion::Return(JsValue::Object(self.0.object.into_handle())) } else { - Completion::Throw(runtime.new_native_error( + Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "proxy preventExtensions handler returned false", )?) } } - NativeConversion::Value(value) => Completion::Return(Value::Bool(value)), + NativeConversion::Value(value) => Completion::Return(JsValue::Bool(value)), })) } pub(crate) fn set( @@ -893,7 +962,9 @@ impl PropertyResume { ) -> Result { if let Phase::AssignSet { state, key } = self.0.phase { if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(PropertyStep::Complete(Completion::Throw(value))); + return Ok(PropertyStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } return { let updated_0 = Phase::Result; @@ -931,7 +1002,7 @@ impl PropertyResume { self.0.object.clone(), key.clone(), value, - Value::Object(self.0.object.clone()), + js_object_value(runtime, &self.0.object)?, { let updated_0 = Phase::AssignSet { state, key }; self.0.phase = updated_0; @@ -947,13 +1018,14 @@ impl PropertyResume { Completion::Return(value) => value, }; let value = if let Some(pair) = pair { + let element = runtime.root_and_release_jsvalue(value)?; runtime.define_fresh_object_keys_array_element( &pair, 1, - value, + element, "fresh Object.entries pair rejected its value", )?; - Value::Object(pair) + JsValue::Object(pair.into_handle()) } else { value }; @@ -988,7 +1060,7 @@ pub(in crate::engine::builtins) fn finish( let value = resume.take_key_value(); resume.key( runtime, - runtime.to_primitive( + runtime.to_primitive_jsvalue( realm, value, crate::engine::vm::ToPrimitiveHint::String, @@ -997,12 +1069,17 @@ pub(in crate::engine::builtins) fn finish( } PropertyStep::Convert { mut resume } => { let value = resume.take_convert_value(); - resume.converted(runtime.native_to_property_descriptor(realm, value)?)? + let value = runtime.root_and_release_jsvalue(value)?; + resume.converted( + runtime, + runtime.native_to_property_descriptor(realm, value)?, + )? } PropertyStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(receiver)?; resume.read( runtime, runtime.internal_get(realm, &object, &key, receiver)?, @@ -1013,6 +1090,8 @@ pub(in crate::engine::builtins) fn finish( let key = resume.take_set_key(); let value = resume.take_set_value(); let receiver = resume.take_set_receiver(); + let value = runtime.root_and_release_jsvalue(value)?; + let receiver = runtime.root_and_release_jsvalue(receiver)?; resume.set( runtime, runtime.internal_set(realm, &object, &key, value, receiver)?, @@ -1078,7 +1157,7 @@ mod tests { let target_id = target.object_id(); let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::Object(target)], + readable: vec![runtime.into_jsvalue(Value::Object(target)).unwrap()], }; let PropertyStep::Keys { mut resume } = PropertyStep::start( &runtime, @@ -1091,7 +1170,9 @@ mod tests { }; let _ = resume.take_keys_object(); - drop(arguments); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } let key = runtime.intern_property_key("x").unwrap(); let PropertyStep::Descriptor { mut resume } = resume .keys(&runtime, NativeConversion::Value(vec![key])) @@ -1118,7 +1199,9 @@ mod tests { }; let _ = resume.take_read_object(); let _ = resume.take_read_key(); - let _ = resume.take_read_receiver(); + runtime + .release_jsvalue(resume.take_read_receiver()) + .unwrap(); let Phase::Entry { state, @@ -1143,18 +1226,18 @@ mod tests { } } -#[derive(Default)] struct PropertyStepPending { + runtime: Runtime, keys_object: Option, - key_value: Option, - convert_value: Option, + key_value: Option, + convert_value: Option, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, set_object: Option, set_key: Option, - set_value: Option, - set_receiver: Option, + set_value: Option, + set_receiver: Option, has_object: Option, has_key: Option, delete_object: Option, @@ -1167,23 +1250,73 @@ struct PropertyStepPending { extensible_object: Option, prevent_object: Option, } +impl PropertyStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + keys_object: None, + key_value: None, + convert_value: None, + read_object: None, + read_key: None, + read_receiver: None, + set_object: None, + set_key: None, + set_value: None, + set_receiver: None, + has_object: None, + has_key: None, + delete_object: None, + delete_key: None, + define_object: None, + define_key: None, + define_descriptor: None, + descriptor_object: None, + descriptor_key: None, + extensible_object: None, + prevent_object: None, + } + } +} +impl Drop for PropertyStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow. + fn drop(&mut self) { + if let Some(value) = self.key_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.convert_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.set_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.set_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} impl PropertyStep { pub(crate) fn request_keys(object: ObjectRef, mut resume: PropertyResume) -> Self { resume.0.pending_effect.keys_object = Some(object); Self::Keys { resume } } - pub(crate) fn request_key(value: Value, mut resume: PropertyResume) -> Self { + pub(crate) fn request_key(value: JsValue, mut resume: PropertyResume) -> Self { resume.0.pending_effect.key_value = Some(value); Self::Key { resume } } - pub(crate) fn request_convert(value: Value, mut resume: PropertyResume) -> Self { + pub(crate) fn request_convert(value: JsValue, mut resume: PropertyResume) -> Self { resume.0.pending_effect.convert_value = Some(value); Self::Convert { resume } } pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: PropertyResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -1194,8 +1327,8 @@ impl PropertyStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, - receiver: Value, + value: JsValue, + receiver: JsValue, mut resume: PropertyResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -1259,14 +1392,14 @@ impl PropertyResume { .take() .expect("PropertyStep Keys object") } - pub(crate) fn take_key_value(&mut self) -> Value { + pub(crate) fn take_key_value(&mut self) -> JsValue { self.0 .pending_effect .key_value .take() .expect("PropertyStep Key value") } - pub(crate) fn take_convert_value(&mut self) -> Value { + pub(crate) fn take_convert_value(&mut self) -> JsValue { self.0 .pending_effect .convert_value @@ -1287,7 +1420,7 @@ impl PropertyResume { .take() .expect("PropertyStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -1308,14 +1441,14 @@ impl PropertyResume { .take() .expect("PropertyStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value .take() .expect("PropertyStep Set value") } - pub(crate) fn take_set_receiver(&mut self) -> Value { + pub(crate) fn take_set_receiver(&mut self) -> JsValue { self.0 .pending_effect .set_receiver diff --git a/src/engine/builtins/object/prototype.rs b/src/engine/builtins/object/prototype.rs index d2da2bb3..b75578c4 100644 --- a/src/engine/builtins/object/prototype.rs +++ b/src/engine/builtins/object/prototype.rs @@ -5,7 +5,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::ObjectRef, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -93,19 +93,20 @@ impl BuiltinPrototypeStep { _ => return Self::start(runtime, realm, kind, arguments), }; if matches!(kind, BuiltinPrototypeKind::Setter) { - if matches!(receiver, Value::Null | Value::Undefined) { + if matches!(receiver, JsValue::Null | JsValue::Undefined) { return not_object(runtime, realm); } let prototype = match arguments.readable.first().ok_or(RuntimeError::Invariant( "prototype setter argv was not padded", ))? { - Value::Object(object) => Some(object.clone()), - Value::Null => None, - _ => return Ok(Self::Complete(Completion::Return(Value::Undefined))), + JsValue::Object(id) => Some(ObjectRef::from_borrowed_handle(runtime.clone(), *id)?), + JsValue::Null => None, + _ => return Ok(Self::Complete(Completion::Return(JsValue::Undefined))), }; - let Value::Object(object) = receiver else { - return Ok(Self::Complete(Completion::Return(Value::Undefined))); + let JsValue::Object(id) = receiver else { + return Ok(Self::Complete(Completion::Return(JsValue::Undefined))); }; + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; return Ok(Self::Set { object: object.clone(), prototype, @@ -119,16 +120,23 @@ impl BuiltinPrototypeStep { // isPrototypeOf checks the candidate before converting its receiver. let candidate = if matches!(kind, BuiltinPrototypeKind::IsPrototype) { match arguments.readable.first() { - Some(Value::Object(object)) => Some(object.clone()), - _ => return Ok(Self::Complete(Completion::Return(Value::Bool(false)))), + Some(JsValue::Object(id)) => { + Some(ObjectRef::from_borrowed_handle(runtime.clone(), *id)?) + } + _ => return Ok(Self::Complete(Completion::Return(JsValue::Bool(false)))), } } else { None }; - let object = match runtime.native_to_object(realm, receiver.clone())? { - NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(receiver)?)? { + NativeConversion::Value(object) => object, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; Ok(Self::Get { object: candidate.unwrap_or_else(|| object.clone()), resume: BuiltinPrototypeResume(Box::new(BuiltinPrototypeResumeState { @@ -155,11 +163,11 @@ impl BuiltinPrototypeStep { } _ => "Reflect target argv was not padded", }))?; - if matches!(target, Value::Null | Value::Undefined) + if matches!(target, JsValue::Null | JsValue::Undefined) || (matches!( kind, BuiltinPrototypeKind::ReflectGet | BuiltinPrototypeKind::ReflectSet - ) && !matches!(target, Value::Object(_))) + ) && !matches!(target, JsValue::Object(_))) { return not_object(runtime, realm); } @@ -167,12 +175,15 @@ impl BuiltinPrototypeStep { kind, BuiltinPrototypeKind::ObjectGet | BuiltinPrototypeKind::ReflectGet ) { - let object = match runtime.native_to_object(realm, target.clone())? { - NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); - } - }; + let object = + match runtime.native_to_object_jsvalue(realm, runtime.dup_jsvalue(target)?)? { + NativeConversion::Value(object) => object, + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + }; return Ok(Self::Get { object: object.clone(), resume: BuiltinPrototypeResume(Box::new(BuiltinPrototypeResumeState { @@ -185,13 +196,16 @@ impl BuiltinPrototypeStep { let prototype = match arguments.readable.get(1).ok_or(RuntimeError::Invariant( "Object.setPrototypeOf prototype argv was not padded", ))? { - Value::Object(object) => Some(object.clone()), - Value::Null => None, + JsValue::Object(id) => Some(ObjectRef::from_borrowed_handle(runtime.clone(), *id)?), + JsValue::Null => None, _ => return not_object(runtime, realm), }; - let Value::Object(object) = target else { - return Ok(Self::Complete(Completion::Return(target.clone()))); + let JsValue::Object(id) = target else { + return Ok(Self::Complete(Completion::Return( + runtime.dup_jsvalue(target)?, + ))); }; + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; Ok(Self::Set { object: object.clone(), prototype, @@ -205,12 +219,13 @@ impl BuiltinPrototypeStep { } fn not_object(runtime: &Runtime, realm: ContextId) -> Result { Ok(BuiltinPrototypeStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))) } impl BuiltinPrototypeResume { pub(crate) fn prototype( self, + runtime: &Runtime, result: NativeConversion>, ) -> Result { if !matches!( @@ -227,13 +242,13 @@ impl BuiltinPrototypeResume { if matches!(self.0.kind, BuiltinPrototypeKind::IsPrototype) { return Ok(match result { NativeConversion::Throw(value) => { - BuiltinPrototypeStep::Complete(Completion::Throw(value)) + BuiltinPrototypeStep::Complete(Completion::Throw(runtime.into_jsvalue(value)?)) } NativeConversion::Value(None) => { - BuiltinPrototypeStep::Complete(Completion::Return(Value::Bool(false))) + BuiltinPrototypeStep::Complete(Completion::Return(JsValue::Bool(false))) } NativeConversion::Value(Some(object)) if object == self.0.object => { - BuiltinPrototypeStep::Complete(Completion::Return(Value::Bool(true))) + BuiltinPrototypeStep::Complete(Completion::Return(JsValue::Bool(true))) } NativeConversion::Value(Some(object)) => BuiltinPrototypeStep::Get { object, @@ -243,9 +258,11 @@ impl BuiltinPrototypeResume { } Ok(BuiltinPrototypeStep::Complete(match result { NativeConversion::Value(prototype) => { - Completion::Return(prototype.map_or(Value::Null, Value::Object)) + Completion::Return(prototype.map_or(JsValue::Null, |object| { + JsValue::Object(object.into_handle()) + })) } - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), })) } pub(crate) fn boolean( @@ -255,17 +272,17 @@ impl BuiltinPrototypeResume { ) -> Result { Ok(BuiltinPrototypeStep::Complete(match self.0.kind { BuiltinPrototypeKind::ReflectSet => match result { - NativeConversion::Value(value) => Completion::Return(Value::Bool(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => Completion::Return(JsValue::Bool(value)), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), }, BuiltinPrototypeKind::ObjectSet | BuiltinPrototypeKind::Setter => { match runtime.finish_set_prototype_or_throw(self.0.realm, &self.0.object, result)? { - Some(value) => Completion::Throw(value), + Some(value) => Completion::Throw(runtime.into_jsvalue(value)?), None => { Completion::Return(if matches!(self.0.kind, BuiltinPrototypeKind::Setter) { - Value::Undefined + JsValue::Undefined } else { - Value::Object(self.0.object) + JsValue::Object(self.0.object.into_handle()) }) } } @@ -287,7 +304,7 @@ pub(in crate::engine::builtins) fn finish( step = match step { BuiltinPrototypeStep::Complete(result) => return Ok(result), BuiltinPrototypeStep::Get { object, resume } => { - resume.prototype(runtime.internal_get_prototype_of(realm, &object)?)? + resume.prototype(runtime, runtime.internal_get_prototype_of(realm, &object)?)? } BuiltinPrototypeStep::Set { object, diff --git a/src/engine/builtins/object/string.rs b/src/engine/builtins/object/string.rs index eb3355bb..0e223450 100644 --- a/src/engine/builtins/object/string.rs +++ b/src/engine/builtins/object/string.rs @@ -5,7 +5,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{PropertyKey, WellKnownSymbol}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{DirectCallTarget, NativeInvocation}, @@ -46,7 +46,7 @@ const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct ObjectStringResumeState { pending_effect: ObjectStringStepPending, realm: ContextId, - receiver: Value, + receiver: JsValue, phase: Phase, } enum Phase { @@ -54,12 +54,12 @@ enum Phase { LocaleMethod, LocaleResult, } -fn tag_string(tag: JsString) -> Result { +fn tag_string(runtime: &Runtime, tag: JsString) -> Result { let value = JsString::from_static("[object ") .try_concat(&tag)? .try_concat(&JsString::from_static("]"))?; Ok(ObjectStringStep::Complete(Completion::Return( - Value::String(value), + runtime.unroot_value(&Value::String(value))?, ))) } impl ObjectStringStep { @@ -74,28 +74,37 @@ impl ObjectStringStep { "Object string conversion did not receive a call", )); }; + let this_value = runtime.dup_jsvalue(this_value)?; match kind { ObjectStringKind::Tag => { match this_value { - Value::Undefined => return tag_string(JsString::from_static("Undefined")), - Value::Null => return tag_string(JsString::from_static("Null")), + JsValue::Undefined => { + return tag_string(runtime, JsString::from_static("Undefined")); + } + JsValue::Null => return tag_string(runtime, JsString::from_static("Null")), _ => {} } - let object = match runtime.native_to_object(realm, this_value.clone())? { + let object = match runtime + .native_to_object(realm, runtime.root_and_release_jsvalue(this_value)?)? + { NativeConversion::Value(object) => object, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let tag = match runtime.object_default_to_string_tag(realm, &object)? { NativeConversion::Value(tag) => tag, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; - let receiver = Value::Object(object); + let receiver = runtime.into_jsvalue(Value::Object(object))?; Ok(Self::request_read( - receiver.clone(), + runtime.dup_jsvalue(&receiver)?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::ToStringTag)), ObjectStringResume(Box::new(ObjectStringResumeState { pending_effect: ObjectStringStepPending::default(), @@ -106,24 +115,25 @@ impl ObjectStringStep { )) } ObjectStringKind::Locale => { - if matches!(this_value, Value::Null | Value::Undefined) { - let message = if matches!(this_value, Value::Null) { + if matches!(this_value, JsValue::Null | JsValue::Undefined) { + let message = if matches!(this_value, JsValue::Null) { "cannot read property 'toString' of null" } else { "cannot read property 'toString' of undefined" }; return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, message)?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, message)?, ))); } + let seen = runtime.dup_jsvalue(&this_value)?; Ok(Self::request_read( - this_value.clone(), + seen, runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::ToString)?, ObjectStringResume(Box::new(ObjectStringResumeState { pending_effect: ObjectStringStepPending::default(), realm, - receiver: this_value.clone(), + receiver: this_value, phase: Phase::LocaleMethod, })), )) @@ -138,17 +148,22 @@ impl ObjectStringResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(ObjectStringStep::Complete(Completion::Throw(value))); } }; match self.0.phase { - Phase::Tag(default_tag) => tag_string(match value { - Value::String(tag) => tag, - _ => default_tag, - }), - Phase::LocaleResult => Ok(ObjectStringStep::Complete(Completion::Return(value))), + Phase::Tag(default_tag) => tag_string( + runtime, + match value { + Value::String(tag) => tag, + _ => default_tag, + }, + ), + Phase::LocaleResult => Ok(ObjectStringStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), Phase::LocaleMethod => { let callable = match value { Value::Object(object) => runtime.as_callable(&object)?, @@ -156,7 +171,7 @@ impl ObjectStringResume { }; let Some(callable) = callable else { return Ok(ObjectStringStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, "not a function", @@ -165,7 +180,7 @@ impl ObjectStringResume { }; Ok(ObjectStringStep::request_call( DirectCallTarget::Callable(callable), - self.0.receiver.clone(), + runtime.dup_jsvalue(&self.0.receiver)?, { let updated_0 = Phase::LocaleResult; self.0.phase = updated_0; @@ -185,7 +200,7 @@ pub(super) fn finish( step = match step { ObjectStringStep::Complete(result) => return Ok(result), ObjectStringStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -194,7 +209,7 @@ pub(super) fn finish( } ObjectStringStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; { let result = match target { DirectCallTarget::Callable(callable) => { @@ -213,14 +228,14 @@ pub(super) fn finish( #[derive(Default)] struct ObjectStringStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, call_target: Option, - call_receiver: Option, + call_receiver: Option, } impl ObjectStringStep { pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: ObjectStringResume, ) -> Self { @@ -230,7 +245,7 @@ impl ObjectStringStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, + receiver: JsValue, mut resume: ObjectStringResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -239,7 +254,7 @@ impl ObjectStringStep { } } impl ObjectStringResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -260,7 +275,7 @@ impl ObjectStringResume { .take() .expect("ObjectStringStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver diff --git a/src/engine/builtins/object/tests.rs b/src/engine/builtins/object/tests.rs index 16ff6e18..80c83b1e 100644 --- a/src/engine/builtins/object/tests.rs +++ b/src/engine/builtins/object/tests.rs @@ -1,4 +1,5 @@ use crate::engine::api::Context; +use crate::engine::atom::AtomIdx; use crate::engine::heap::{AutoInitProperty, PropertySlot, RawValue}; use crate::engine::object::shape::PropertyFlags; @@ -40,20 +41,29 @@ fn reduced_group_by_element_limit_checks_before_next_and_preserves_throw() { ); let arguments = NativeArguments { actual_arg_count: 2, - readable: vec![Value::Object(iterable), Value::Object(callback)], + readable: vec![ + runtime.into_jsvalue(Value::Object(iterable)).unwrap(), + runtime.into_jsvalue(Value::Object(callback)).unwrap(), + ], }; let completion = runtime .call_object_group_by_with_element_limit( context.realm, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &arguments, 2, ) .unwrap(); - let Completion::Throw(Value::Object(error)) = completion else { + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + let Completion::Throw(value) = completion else { + panic!("reduced Object.groupBy limit did not throw an Error object"); + }; + let Value::Object(error) = runtime.root_and_release_jsvalue(value).unwrap() else { panic!("reduced Object.groupBy limit did not throw an Error object"); }; assert_eq!( @@ -91,7 +101,8 @@ fn object_keys_family_autoinit_preserves_pinned_metadata() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -133,7 +144,8 @@ fn object_extensibility_autoinit_preserves_pinned_metadata() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -233,7 +245,8 @@ fn object_descriptor_statics_autoinit_preserve_pinned_metadata() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -275,7 +288,8 @@ fn object_is_autoinit_and_same_value_semantics_match_pinned_quickjs() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -338,7 +352,8 @@ fn object_assign_autoinit_and_ordinary_snapshot_semantics_match_pinned_quickjs() let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -408,7 +423,8 @@ fn object_assign_autoinit_and_ordinary_snapshot_semantics_match_pinned_quickjs() let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::AutoInit(AutoInitProperty::NativeBuiltin { @@ -477,7 +493,8 @@ fn object_from_entries_autoinit_preserves_pinned_metadata() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -514,7 +531,12 @@ fn object_has_own_autoinit_preserves_pinned_metadata_and_presence_is_non_materia let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(has_own_key.atom()).unwrap()).unwrap(); + let slot_index = usize::try_from( + shape + .find(AtomIdx::from_raw(has_own_key.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -541,7 +563,12 @@ fn object_has_own_autoinit_preserves_pinned_metadata_and_presence_is_non_materia let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(keys_key.atom()).unwrap()).unwrap(); + let slot_index = usize::try_from( + shape + .find(AtomIdx::from_raw(keys_key.atom().raw())) + .unwrap(), + ) + .unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::AutoInit(AutoInitProperty::NativeBuiltin { @@ -693,7 +720,8 @@ fn object_integrity_autoinit_materializes_and_tightens_in_pinned_order() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -719,7 +747,8 @@ fn object_integrity_autoinit_materializes_and_tightens_in_pinned_order() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, false), @@ -747,7 +776,8 @@ fn object_integrity_autoinit_materializes_and_tightens_in_pinned_order() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(false, false, false), @@ -777,7 +807,8 @@ fn object_is_sealed_scans_descriptors_before_extensibility_and_short_circuits_au let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::AutoInit( @@ -814,8 +845,14 @@ fn object_is_sealed_scans_descriptors_before_extensibility_and_short_circuits_au let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let create_slot = usize::try_from(shape.find(create.atom()).unwrap()).unwrap(); - let get_prototype_slot = usize::try_from(shape.find(get_prototype_of.atom()).unwrap()).unwrap(); + let create_slot = + usize::try_from(shape.find(AtomIdx::from_raw(create.atom().raw())).unwrap()).unwrap(); + let get_prototype_slot = usize::try_from( + shape + .find(AtomIdx::from_raw(get_prototype_of.atom().raw())) + .unwrap(), + ) + .unwrap(); assert!(matches!( object.slots.get(create_slot), Some(PropertySlot::Data(RawValue::Object(_))) @@ -1135,7 +1172,8 @@ fn object_keys_descriptor_recheck_materializes_non_enumerable_autoinits() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert!(matches!( object.slots.get(slot_index), Some(PropertySlot::AutoInit( @@ -1157,7 +1195,8 @@ fn object_keys_descriptor_recheck_materializes_non_enumerable_autoinits() { let state = runtime.0.state.borrow(); let object = state.heap.object(object_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); let Some(PropertySlot::Data(RawValue::Object(function))) = object.slots.get(slot_index) else { panic!("Object.{name} was not materialized during descriptor recheck"); @@ -1284,7 +1323,10 @@ fn borrowed_object_entries_uses_its_defining_realm_for_arrays_and_errors() { &[Value::String(JsString::from_static("x"))], ) .unwrap(); - let Completion::Return(Value::Object(result)) = completion else { + let Completion::Return(value) = completion else { + panic!("borrowed Object.entries did not return an Array"); + }; + let Value::Object(result) = runtime.root_and_release_jsvalue(value).unwrap() else { panic!("borrowed Object.entries did not return an Array"); }; assert_eq!( @@ -1310,7 +1352,10 @@ fn borrowed_object_entries_uses_its_defining_realm_for_arrays_and_errors() { &[Value::Undefined], ) .unwrap(); - let Completion::Throw(Value::Object(error)) = completion else { + let Completion::Throw(value) = completion else { + panic!("borrowed Object.entries nullish conversion did not throw"); + }; + let Value::Object(error) = runtime.root_and_release_jsvalue(value).unwrap() else { panic!("borrowed Object.entries nullish conversion did not throw"); }; assert_eq!( diff --git a/src/engine/builtins/primitive.rs b/src/engine/builtins/primitive.rs index 39364eb9..b3c41c37 100644 --- a/src/engine/builtins/primitive.rs +++ b/src/engine/builtins/primitive.rs @@ -12,7 +12,7 @@ use crate::engine::object::SymbolRef; use crate::engine::object::access::raw_string_property_one_level; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation, NativeInvokeOutcome}; @@ -29,17 +29,15 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - constructor::finish( - self, - realm, - constructor::PrimitiveConstructorStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + constructor::finish( self, realm, - kind, - &invocation, - arguments, - )?, - ) + constructor::PrimitiveConstructorStep::start( + self, realm, kind, invocation, arguments, + )?, + ) + }) } pub(crate) fn new_not_constructor_error( @@ -73,17 +71,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - globals::finish( - self, - realm, - globals::GlobalStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + globals::finish( self, realm, - globals::GlobalKind::Parse(kind), - &invocation, - arguments, - )?, - ) + globals::GlobalStep::start( + self, + realm, + globals::GlobalKind::Parse(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_global_number_predicate( @@ -93,17 +93,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - globals::finish( - self, - realm, - globals::GlobalStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + globals::finish( self, realm, - globals::GlobalKind::Predicate(kind), - &invocation, - arguments, - )?, - ) + globals::GlobalStep::start( + self, + realm, + globals::GlobalKind::Predicate(kind), + invocation, + arguments, + )?, + ) + }) } fn finish_global_uri_codec( @@ -125,9 +127,11 @@ impl Runtime { GlobalUriCodecKind::Unescape => crate::engine::builtins::uri::unescape(&input), }; match result { - Ok(value) => Ok(Completion::Return(Value::String(value))), + Ok(value) => Ok(Completion::Return( + self.unroot_value(&Value::String(value))?, + )), Err(crate::engine::builtins::uri::UriCodecError::String(error)) => Err(error.into()), - Err(error) => Ok(Completion::Throw(self.new_native_error( + Err(error) => Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Uri, error.message(), @@ -142,17 +146,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - globals::finish( - self, - realm, - globals::GlobalStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + globals::finish( self, realm, - globals::GlobalKind::Uri(kind), - &invocation, - arguments, - )?, - ) + globals::GlobalStep::start( + self, + realm, + globals::GlobalKind::Uri(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn primitive_this_value( @@ -279,17 +285,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - text::finish( - self, - realm, - text::ScalarTextStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::ScalarTextKind::CharAt(selector), - &invocation, - arguments, - )?, - ) + text::ScalarTextStep::start( + self, + realm, + text::ScalarTextKind::CharAt(selector), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_string_prototype_iterator( @@ -297,21 +305,23 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - let arguments = NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }; - text::finish( - self, - realm, - text::ScalarTextStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + let arguments = NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }; + text::finish( self, realm, - text::ScalarTextKind::Iterator, - &invocation, - &arguments, - )?, - ) + text::ScalarTextStep::start( + self, + realm, + text::ScalarTextKind::Iterator, + invocation, + &arguments, + )?, + ) + }) } pub(crate) fn call_string_iterator_next( @@ -321,9 +331,12 @@ impl Runtime { ) -> Result { match self.call_string_iterator_next_raw(realm, invocation)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + Ok(Completion::Return(JsValue::Object( + self.new_iterator_result(realm, value, done)?.into_handle(), + ))) + } } } @@ -341,9 +354,10 @@ impl Runtime { "String Iterator next did not receive an iterator-next invocation", )); }; + let this_value = self.root_value(&this_value)?; let Value::Object(iterator) = this_value else { return Ok(NativeInvokeOutcome::Completion(Completion::Throw( - self.new_native_error( + self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "String Iterator object expected", @@ -361,7 +375,7 @@ impl Runtime { ); if !branded { return Ok(NativeInvokeOutcome::Completion(Completion::Throw( - self.new_native_error( + self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "String Iterator object expected", @@ -375,8 +389,8 @@ impl Runtime { .heap .string_iterator_next(iterator.object_id())?; let (value, done) = match value { - Some(value) => (Value::String(value), false), - None => (Value::Undefined, true), + Some(value) => (self.unroot_value(&Value::String(value))?, false), + None => (JsValue::Undefined, true), }; Ok(NativeInvokeOutcome::IteratorNextRaw { value, done }) } @@ -387,17 +401,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - text::finish( - self, - realm, - text::ScalarTextStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::ScalarTextKind::CharCodeAt, - &invocation, - arguments, - )?, - ) + text::ScalarTextStep::start( + self, + realm, + text::ScalarTextKind::CharCodeAt, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_string_prototype_code_point_at( @@ -406,17 +422,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - text::finish( - self, - realm, - text::ScalarTextStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::ScalarTextKind::CodePointAt, - &invocation, - arguments, - )?, - ) + text::ScalarTextStep::start( + self, + realm, + text::ScalarTextKind::CodePointAt, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_string_prototype_concat( @@ -425,17 +443,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - text::finish( - self, - realm, - text::ScalarTextStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::ScalarTextKind::Concat, - &invocation, - arguments, - )?, - ) + text::ScalarTextStep::start( + self, + realm, + text::ScalarTextKind::Concat, + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_string_prototype_well_formed( @@ -444,21 +464,23 @@ impl Runtime { selector: StringWellFormedKind, invocation: NativeInvocation, ) -> Result { - let arguments = NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }; - text::finish( - self, - realm, - text::ScalarTextStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + let arguments = NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }; + text::finish( self, realm, - text::ScalarTextKind::WellFormed(selector), - &invocation, - &arguments, - )?, - ) + text::ScalarTextStep::start( + self, + realm, + text::ScalarTextKind::WellFormed(selector), + invocation, + &arguments, + )?, + ) + }) } fn finish_branded_to_string( @@ -488,24 +510,26 @@ impl Runtime { })?; JsString::checked_length(0, formatted.len())?; debug_assert!(formatted.is_ascii()); - Ok(Completion::Return(Value::String( + Ok(Completion::Return(self.unroot_value(&Value::String( JsString::from_owned_latin1(formatted.into_bytes()), - ))) + ))?)) } - (PrimitiveKind::String, Value::String(value)) => { - Ok(Completion::Return(Value::String(value))) + (PrimitiveKind::String, Value::String(value)) => Ok(Completion::Return( + self.unroot_value(&Value::String(value))?, + )), + (PrimitiveKind::Boolean, Value::Bool(value)) => { + Ok(Completion::Return(self.unroot_value(&Value::String( + JsString::from_static(if value { "true" } else { "false" }), + ))?)) } - (PrimitiveKind::Boolean, Value::Bool(value)) => Ok(Completion::Return(Value::String( - JsString::from_static(if value { "true" } else { "false" }), - ))), - (PrimitiveKind::Symbol, Value::Symbol(value)) => Ok(Completion::Return(Value::String( - self.symbol_descriptive_string(&value)?, - ))), + (PrimitiveKind::Symbol, Value::Symbol(value)) => Ok(Completion::Return( + self.unroot_value(&Value::String(self.symbol_descriptive_string(&value)?))?, + )), (PrimitiveKind::BigInt, Value::BigInt(value)) => { if value.exceeds_allocation_limit() && (value.is_negative() || !radix.is_power_of_two()) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "BigInt is too large to allocate", @@ -516,9 +540,9 @@ impl Runtime { .map_err(|_| RuntimeError::Invariant("validated BigInt radix was rejected"))?; JsString::checked_length(0, text.len())?; debug_assert!(text.is_ascii()); - Ok(Completion::Return(Value::String( + Ok(Completion::Return(self.unroot_value(&Value::String( JsString::from_owned_latin1(text.into_bytes()), - ))) + ))?)) } _ => Err(RuntimeError::Invariant( "unimplemented primitive toString reached native dispatch", @@ -533,17 +557,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - numeric::finish( - self, - realm, - numeric::NumericStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + numeric::finish( self, realm, - numeric::NumericKind::ToString(kind), - &invocation, - arguments, - )?, - ) + numeric::NumericStep::start( + self, + realm, + numeric::NumericKind::ToString(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn finish_number_format( @@ -555,19 +581,19 @@ impl Runtime { Ok(value) => { JsString::checked_length(0, value.len())?; debug_assert!(value.is_ascii()); - Ok(Completion::Return(Value::String( + Ok(Completion::Return(self.unroot_value(&Value::String( JsString::from_owned_latin1(value.into_bytes()), - ))) + ))?)) } Err(crate::engine::value::number::NumberFormatError::InvalidDigits) => { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid number of digits", )?)) } Err(crate::engine::value::number::NumberFormatError::InvalidRadix) => { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "radix must be between 2 and 36", @@ -583,17 +609,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - numeric::finish( - self, - realm, - numeric::NumericStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + numeric::finish( self, realm, - numeric::NumericKind::Format(kind), - &invocation, - arguments, - )?, - ) + numeric::NumericStep::start( + self, + realm, + numeric::NumericKind::Format(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_number_predicate( @@ -620,7 +648,7 @@ impl Runtime { && number.abs() <= 9_007_199_254_740_991.0 } }); - Ok(Completion::Return(Value::Bool(result))) + Ok(Completion::Return(JsValue::Bool(result))) } pub(crate) fn call_bigint_as_n( @@ -630,17 +658,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - numeric::finish( - self, - realm, - numeric::NumericStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + numeric::finish( self, realm, - numeric::NumericKind::BigIntAsN(kind), - &invocation, - arguments, - )?, - ) + numeric::NumericStep::start( + self, + realm, + numeric::NumericKind::BigIntAsN(kind), + invocation, + arguments, + )?, + ) + }) } pub(crate) fn call_symbol_registry( @@ -667,23 +697,24 @@ impl Runtime { realm, globals::GlobalKind::SymbolFor, &NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, arguments, )?, ), SymbolRegistryKind::KeyFor => { + let argument = self.root_value(argument)?; let Value::Symbol(symbol) = argument else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not a symbol", )?)); }; - Ok(Completion::Return( - self.symbol_key_for(symbol)? - .map_or(Value::Undefined, Value::String), - )) + Ok(Completion::Return(match self.symbol_key_for(&symbol)? { + Some(value) => self.unroot_value(&Value::String(value))?, + None => JsValue::Undefined, + })) } } } @@ -698,19 +729,24 @@ impl Runtime { "Symbol.prototype.description received the wrong native invocation", )); }; + let this_value = self.root_value(this_value)?; let value = - match self.primitive_this_value_borrowed(realm, PrimitiveKind::Symbol, this_value)? { + match self.primitive_this_value_borrowed(realm, PrimitiveKind::Symbol, &this_value)? { NativeConversion::Value(Value::Symbol(value)) => value, NativeConversion::Value(_) => { return Err(RuntimeError::Invariant( "Symbol brand extraction did not return a Symbol", )); } - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; Ok(Completion::Return( - self.symbol_description(&value)? - .map_or(Value::Undefined, Value::String), + match self.symbol_description(&value)? { + Some(value) => self.unroot_value(&Value::String(value))?, + None => JsValue::Undefined, + }, )) } @@ -725,9 +761,10 @@ impl Runtime { "primitive valueOf did not receive a generic invocation", )); }; - match self.primitive_this_value_borrowed(realm, kind, this_value)? { - NativeConversion::Value(value) => Ok(Completion::Return(value)), - NativeConversion::Throw(value) => Ok(Completion::Throw(value)), + let this_value = self.root_value(this_value)?; + match self.primitive_this_value_borrowed(realm, kind, &this_value)? { + NativeConversion::Value(value) => Ok(Completion::Return(self.into_jsvalue(value)?)), + NativeConversion::Throw(value) => Ok(Completion::Throw(self.into_jsvalue(value)?)), } } @@ -752,26 +789,29 @@ impl Runtime { ) -> Result { use super::function::invoke::InvokeStep; let completion = match arguments.readable.first() { - Some(Value::Object(value)) - if matches!(arguments.readable.get(1), Some(Value::Bool(false))) => + Some(JsValue::Object(value)) + if matches!(arguments.readable.get(1), Some(JsValue::Bool(false))) => { - Ok(Completion::Throw(Value::Object(value.clone()))) + Ok(Completion::Throw( + self.dup_jsvalue(&JsValue::Object(*value))?, + )) } - Some(Value::Object(callback)) => { - let callback = self.callable_from_value(Value::Object(callback.clone()))?; + Some(JsValue::Object(callback)) => { + let callback = + self.callable_from_value(self.root_value(&JsValue::Object(*callback))?)?; let active_function = self.active_function()?; return Ok(InvokeStep::Call(Box::new( super::function::invoke::InvokeCall { target: crate::engine::vm::call::DirectCallTarget::Callable(callback), - receiver: Value::Undefined, - arguments: vec![Value::Object(active_function)], + receiver: JsValue::Undefined, + arguments: vec![self.unroot_value(&Value::Object(active_function))?], }, ))); } - Some(Value::Bool(false)) => Ok(Completion::Throw(Value::String( - JsString::from_static("active frame probe throw"), - ))), - Some(Value::Bool(true)) => { + Some(JsValue::Bool(false)) => Ok(Completion::Throw(self.unroot_value( + &Value::String(JsString::from_static("active frame probe throw")), + )?)), + Some(JsValue::Bool(true)) => { Err(RuntimeError::Invariant("active frame probe engine error")) } Some(_) => Err(RuntimeError::Invariant( @@ -784,7 +824,7 @@ impl Runtime { .borrow_mut() .active_frame_probe_snapshots .push(snapshot); - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } }?; Ok(InvokeStep::Complete(completion)) diff --git a/src/engine/builtins/primitive/constructor.rs b/src/engine/builtins/primitive/constructor.rs index b574ede2..b11c7580 100644 --- a/src/engine/builtins/primitive/constructor.rs +++ b/src/engine/builtins/primitive/constructor.rs @@ -3,8 +3,9 @@ use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, builtins::native::PrimitiveKind, heap::ContextId, + object::ObjectRef, object::PropertyKey, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -37,8 +38,8 @@ pub(crate) struct PrimitiveConstructorResumeState { pending_effect: PrimitiveConstructorStepPending, realm: ContextId, kind: PrimitiveKind, - new_target: Value, - value: Value, + new_target: JsValue, + value: JsValue, phase: Phase, } impl PrimitiveConstructorStep { @@ -49,79 +50,76 @@ impl PrimitiveConstructorStep { invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "primitive constructor argv was not padded", - ))?; + let argument = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("primitive constructor argv was not padded"), + )?)?; let NativeInvocation::Construct { new_target } = invocation else { return Err(RuntimeError::Invariant( "primitive constructor requires constructor-or-function invocation", )); }; + let new_target_value = runtime.dup_jsvalue(new_target)?; if matches!(kind, PrimitiveKind::Symbol | PrimitiveKind::BigInt) - && !matches!(new_target, Value::Undefined) + && !matches!(new_target_value, JsValue::Undefined) { - return Ok(Self::Complete(Completion::Throw( - runtime.new_not_constructor_error(realm, new_target)?, - ))); + return Ok(Self::Complete(Completion::Throw(runtime.into_jsvalue( + runtime.new_not_constructor_error( + realm, + &runtime.root_and_release_jsvalue(new_target_value)?, + )?, + )?))); } let resume = PrimitiveConstructorResume(Box::new(PrimitiveConstructorResumeState { pending_effect: PrimitiveConstructorStepPending::default(), realm, kind, - new_target: new_target.clone(), - value: Value::Undefined, + new_target: new_target_value, + value: JsValue::Undefined, phase: Phase::Value, })); match kind { PrimitiveKind::Boolean => { - resume.converted(runtime, Value::Bool(runtime.value_to_boolean(&argument)?)) + let value = JsValue::Bool(runtime.value_to_boolean_jsvalue(&argument)?); + runtime.release_jsvalue(argument)?; + resume.converted(runtime, value) } PrimitiveKind::Number if arguments.actual_arg_count == 0 => { - resume.converted(runtime, Value::Int(0)) + resume.converted(runtime, JsValue::Int(0)) } - PrimitiveKind::String if arguments.actual_arg_count == 0 => { - resume.converted(runtime, Value::String(JsString::from_static(""))) + PrimitiveKind::String if arguments.actual_arg_count == 0 => resume.converted( + runtime, + runtime.into_jsvalue(Value::String(JsString::from_static("")))?, + ), + PrimitiveKind::Symbol if matches!(argument, JsValue::Undefined) => { + let symbol = runtime.new_symbol(None)?; + Ok(Self::Complete(Completion::Return( + runtime.unroot_value(&Value::Symbol(symbol))?, + ))) } - PrimitiveKind::Symbol if matches!(argument, Value::Undefined) => Ok(Self::Complete( - Completion::Return(Value::Symbol(runtime.new_symbol(None)?)), - )), PrimitiveKind::String - if matches!(new_target, Value::Undefined) - && matches!(argument, Value::Symbol(_)) => + if matches!(resume.0.new_target, JsValue::Undefined) + && matches!(argument, JsValue::Symbol(_)) => { - let Value::Symbol(symbol) = argument else { + let Value::Symbol(symbol) = runtime.root_and_release_jsvalue(argument)? else { unreachable!() }; - resume.converted( - runtime, - Value::String(runtime.symbol_descriptive_string(&symbol)?), - ) + let description = runtime.symbol_descriptive_string(&symbol)?; + resume.converted(runtime, runtime.into_jsvalue(Value::String(description))?) } PrimitiveKind::String | PrimitiveKind::Symbol => { - if !matches!(argument, Value::Object(_)) { - let result = runtime.native_to_js_string(realm, &argument)?; + if !matches!(argument, JsValue::Object(_)) { + let value = runtime.root_and_release_jsvalue(argument)?; + let result = runtime.native_to_js_string(realm, &value)?; resume.string(runtime, result) } else { - Ok({ - let __pending_field_value = argument; - let __pending_field_resume = resume; - Self::request_string(__pending_field_value, __pending_field_resume) - }) + Ok(Self::request_string(argument, resume)) } } PrimitiveKind::Number | PrimitiveKind::BigInt => { - if !matches!(argument, Value::Object(_)) { + if !matches!(argument, JsValue::Object(_)) { resume.primitive(runtime, Completion::Return(argument)) } else { - Ok({ - let __pending_field_value = argument; - let __pending_field_resume = resume; - Self::request_primitive(__pending_field_value, __pending_field_resume) - }) + Ok(Self::request_primitive(argument, resume)) } } } @@ -144,20 +142,25 @@ impl PrimitiveConstructorResume { return Ok(PrimitiveConstructorStep::Complete(Completion::Throw(value))); } }; + let value = runtime.root_and_release_jsvalue(value)?; let value = match self.0.kind { PrimitiveKind::Number => { match runtime.number_constructor_from_primitive(self.0.realm, &value)? { - NativeConversion::Value(value) => Value::number(value), + NativeConversion::Value(value) => runtime.into_jsvalue(Value::number(value))?, NativeConversion::Throw(value) => { - return Ok(PrimitiveConstructorStep::Complete(Completion::Throw(value))); + return Ok(PrimitiveConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } PrimitiveKind::BigInt => { match runtime.bigint_constructor_from_primitive(self.0.realm, &value)? { - NativeConversion::Value(value) => Value::BigInt(value), + NativeConversion::Value(value) => runtime.into_jsvalue(Value::BigInt(value))?, NativeConversion::Throw(value) => { - return Ok(PrimitiveConstructorStep::Complete(Completion::Throw(value))); + return Ok(PrimitiveConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } @@ -182,42 +185,39 @@ impl PrimitiveConstructorResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(PrimitiveConstructorStep::Complete(Completion::Throw(value))); + return Ok(PrimitiveConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if self.0.kind == PrimitiveKind::Symbol { + let symbol = runtime.new_symbol(Some(value))?; return Ok(PrimitiveConstructorStep::Complete(Completion::Return( - Value::Symbol(runtime.new_symbol(Some(value))?), + runtime.unroot_value(&Value::Symbol(symbol))?, ))); } - self.converted(runtime, Value::String(value)) + self.converted(runtime, runtime.into_jsvalue(Value::String(value))?) } fn converted( mut self, runtime: &Runtime, - value: Value, + value: JsValue, ) -> Result { - if matches!(self.0.new_target, Value::Undefined) { + if matches!(self.0.new_target, JsValue::Undefined) { return Ok(PrimitiveConstructorStep::Complete(Completion::Return( value, ))); } self.0.value = value; self.0.phase = Phase::Prototype; - Ok({ - let __pending_field_receiver = self.0.new_target.clone(); - let __pending_field_key = - runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?; - let __pending_field_resume = self; - PrimitiveConstructorStep::request_read( - __pending_field_receiver, - __pending_field_key, - __pending_field_resume, - ) - }) + Ok(PrimitiveConstructorStep::request_read( + runtime.dup_jsvalue(&self.0.new_target)?, + runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?, + self, + )) } pub(crate) fn resume( - self, + mut self, runtime: &Runtime, result: Completion, ) -> Result { @@ -226,25 +226,45 @@ impl PrimitiveConstructorResume { "primitive constructor prototype phase mismatch", )); } - let prototype = match result { - Completion::Return(Value::Object(object)) => object, + let result_value = match result { + Completion::Return(value) => Some(value), Completion::Throw(value) => { + let new_target = std::mem::replace(&mut self.0.new_target, JsValue::Undefined); + runtime.release_jsvalue(new_target)?; + let primitive = std::mem::replace(&mut self.0.value, JsValue::Undefined); + runtime.release_jsvalue(primitive)?; return Ok(PrimitiveConstructorStep::Complete(Completion::Throw(value))); } - Completion::Return(_) => { - let realm = match runtime - .function_realm_from_value(self.0.realm, &self.0.new_target)? - { + }; + let prototype = match result_value { + Some(JsValue::Object(id)) => { + let new_target = std::mem::replace(&mut self.0.new_target, JsValue::Undefined); + runtime.release_jsvalue(new_target)?; + ObjectRef::from_owned_handle(runtime.clone(), id) + } + other => { + if let Some(value) = other { + runtime.release_jsvalue(value)?; + } + let new_target = std::mem::replace(&mut self.0.new_target, JsValue::Undefined); + let new_target = runtime.root_and_release_jsvalue(new_target)?; + let realm = match runtime.function_realm_from_value(self.0.realm, &new_target)? { NativeConversion::Value(realm) => realm, NativeConversion::Throw(value) => { - return Ok(PrimitiveConstructorStep::Complete(Completion::Throw(value))); + return Ok(PrimitiveConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; runtime.primitive_prototype_for_realm(realm, self.0.kind)? } }; Ok(PrimitiveConstructorStep::Complete(Completion::Return( - Value::Object(runtime.new_primitive_object(&prototype, self.0.kind, self.0.value)?), + JsValue::Object( + runtime + .new_primitive_object_jsvalue(&prototype, self.0.kind, self.0.value)? + .into_handle(), + ), ))) } } @@ -257,7 +277,7 @@ pub(crate) fn finish( step = match step { PrimitiveConstructorStep::Complete(result) => return Ok(result), PrimitiveConstructorStep::Primitive { mut resume } => { - let value = resume.take_primitive_value(); + let value = runtime.root_and_release_jsvalue(resume.take_primitive_value())?; resume.primitive( runtime, runtime.to_primitive( @@ -268,11 +288,11 @@ pub(crate) fn finish( )? } PrimitiveConstructorStep::String { mut resume } => { - let value = resume.take_string_value(); + let value = runtime.root_and_release_jsvalue(resume.take_string_value())?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } PrimitiveConstructorStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -292,11 +312,11 @@ mod local_completion_tests { let runtime = Runtime::new(); let context = runtime.new_context(); let invocation = NativeInvocation::Construct { - new_target: Value::Undefined, + new_target: JsValue::Undefined, }; let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::Int(42)], + readable: vec![JsValue::Int(42)], }; let result = PrimitiveConstructorStep::start( &runtime, @@ -306,9 +326,11 @@ mod local_completion_tests { &arguments, ) .unwrap(); - assert!( - matches!(result, PrimitiveConstructorStep::Complete(Completion::Return(Value::String(value))) if value == JsString::from_static("42")) - ); + let PrimitiveConstructorStep::Complete(Completion::Return(value)) = result else { + panic!("primitive constructor must complete"); + }; + let value = runtime.root_value(&value).unwrap(); + assert!(matches!(value, Value::String(value) if value == JsString::from_static("42"))); } #[test] @@ -330,22 +352,25 @@ mod local_completion_tests { #[derive(Default)] struct PrimitiveConstructorStepPending { - primitive_value: Option, - string_value: Option, - read_receiver: Option, + primitive_value: Option, + string_value: Option, + read_receiver: Option, read_key: Option, } impl PrimitiveConstructorStep { - pub(crate) fn request_primitive(value: Value, mut resume: PrimitiveConstructorResume) -> Self { + pub(crate) fn request_primitive( + value: JsValue, + mut resume: PrimitiveConstructorResume, + ) -> Self { resume.0.pending_effect.primitive_value = Some(value); Self::Primitive { resume } } - pub(crate) fn request_string(value: Value, mut resume: PrimitiveConstructorResume) -> Self { + pub(crate) fn request_string(value: JsValue, mut resume: PrimitiveConstructorResume) -> Self { resume.0.pending_effect.string_value = Some(value); Self::String { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: PrimitiveConstructorResume, ) -> Self { @@ -355,21 +380,21 @@ impl PrimitiveConstructorStep { } } impl PrimitiveConstructorResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .pending_effect .primitive_value .take() .expect("PrimitiveConstructorStep Primitive value") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .pending_effect .string_value .take() .expect("PrimitiveConstructorStep String value") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver diff --git a/src/engine/builtins/primitive/globals.rs b/src/engine/builtins/primitive/globals.rs index 8ac755e1..d82a539d 100644 --- a/src/engine/builtins/primitive/globals.rs +++ b/src/engine/builtins/primitive/globals.rs @@ -7,7 +7,7 @@ use crate::engine::{ GlobalNumberPredicateKind, GlobalUriCodecKind, NumberParseKind, SymbolRegistryKind, }, heap::ContextId, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -33,8 +33,14 @@ impl GlobalKind { } pub(crate) enum GlobalStep { Complete(Completion), - String { value: Value, resume: GlobalResume }, - Number { value: Value, resume: GlobalResume }, + String { + value: JsValue, + resume: GlobalResume, + }, + Number { + value: JsValue, + resume: GlobalResume, + }, } pub(crate) struct GlobalResume(Box); impl std::ops::Deref for GlobalResume { @@ -52,12 +58,12 @@ const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct GlobalResumeState { realm: ContextId, kind: GlobalKind, - radix: Value, + radix: JsValue, input: Option, } impl GlobalStep { pub(crate) fn start( - _runtime: &Runtime, + runtime: &Runtime, realm: ContextId, kind: GlobalKind, invocation: &NativeInvocation, @@ -68,21 +74,16 @@ impl GlobalStep { "global builtin requires generic invocation", )); } - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "global builtin argv was not padded", - ))?; + let value = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("global builtin argv was not padded"), + )?)?; let resume = GlobalResume(Box::new(GlobalResumeState { realm, kind, - radix: arguments - .readable - .get(1) - .cloned() - .unwrap_or(Value::Undefined), + radix: match arguments.readable.get(1) { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, + }, input: None, })); Ok(if matches!(kind, GlobalKind::Predicate(_)) { @@ -101,15 +102,17 @@ impl GlobalStep { use crate::engine::value::conversion::number::NumberStep; loop { self = match self { - Self::String { value, resume } if !matches!(value, Value::Object(_)) => { + Self::String { value, resume } if !matches!(value, JsValue::Object(_)) => { + let value = runtime.root_and_release_jsvalue(value)?; resume.string(runtime, runtime.string_from_primitive(realm, &value)?)? } - Self::Number { value, resume } if !matches!(value, Value::Object(_)) => { + Self::Number { value, resume } if !matches!(value, JsValue::Object(_)) => { + let value = runtime.root_and_release_jsvalue(value)?; let NumberStep::Complete(result) = NumberStep::start(runtime, realm, value)? else { return Err(RuntimeError::Invariant("primitive global number suspended")); }; - resume.number(result)? + resume.number(runtime, result)? } Self::Complete(completion) => { #[cfg(feature = "profiling")] @@ -132,38 +135,52 @@ impl GlobalResume { let input = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(GlobalStep::Complete(Completion::Throw(value))); + return Ok(GlobalStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.kind { GlobalKind::Parse(NumberParseKind::ParseInt) => { self.0.input = Some(input); Ok(GlobalStep::Number { - value: self.0.radix.clone(), + value: runtime.dup_jsvalue(&self.0.radix)?, resume: self, }) } GlobalKind::Parse(NumberParseKind::ParseFloat) => { - Ok(GlobalStep::Complete(Completion::Return(Value::number( - crate::engine::value::number_parse::parse_float(&input), - )))) + Ok(GlobalStep::Complete(Completion::Return( + crate::engine::value::number::operations::Number::compact( + crate::engine::value::number_parse::parse_float(&input), + ) + .into(), + ))) } GlobalKind::Uri(kind) => Ok(GlobalStep::Complete(runtime.finish_global_uri_codec( self.0.realm, kind, input, )?)), - GlobalKind::SymbolFor => Ok(GlobalStep::Complete(Completion::Return(Value::Symbol( - runtime.symbol_for(&input)?, - )))), + GlobalKind::SymbolFor => { + let symbol = runtime.symbol_for(&input)?; + Ok(GlobalStep::Complete(Completion::Return( + runtime.unroot_value(&Value::Symbol(symbol))?, + ))) + } _ => Err(RuntimeError::Invariant("global string reply mismatch")), } } - pub(crate) fn number(self, result: NativeConversion) -> Result { + pub(crate) fn number( + self, + runtime: &Runtime, + result: NativeConversion, + ) -> Result { let number = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(GlobalStep::Complete(Completion::Throw(value))); + return Ok(GlobalStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let value = match self.0.kind { @@ -182,7 +199,9 @@ impl GlobalResume { }), _ => return Err(RuntimeError::Invariant("global number reply mismatch")), }; - Ok(GlobalStep::Complete(Completion::Return(value))) + Ok(GlobalStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))) } } pub(crate) fn finish( @@ -194,10 +213,12 @@ pub(crate) fn finish( step = match step { GlobalStep::Complete(result) => return Ok(result), GlobalStep::String { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } GlobalStep::Number { value, resume } => { - resume.number(runtime.native_to_number(realm, &value)?)? + let value = runtime.root_and_release_jsvalue(value)?; + resume.number(runtime, runtime.native_to_number(realm, &value)?)? } }; } diff --git a/src/engine/builtins/primitive/numeric.rs b/src/engine/builtins/primitive/numeric.rs index 1aa124e6..14eee3d1 100644 --- a/src/engine/builtins/primitive/numeric.rs +++ b/src/engine/builtins/primitive/numeric.rs @@ -5,7 +5,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, builtins::native::{BigIntAsNKind, NumberFormatKind, PrimitiveKind}, heap::ContextId, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -29,8 +29,14 @@ impl NumericKind { } pub(crate) enum NumericStep { Complete(Completion), - Number { value: Value, resume: NumericResume }, - Primitive { value: Value, resume: NumericResume }, + Number { + value: JsValue, + resume: NumericResume, + }, + Primitive { + value: JsValue, + resume: NumericResume, + }, } enum Phase { Radix, @@ -72,26 +78,29 @@ impl NumericStep { "scalar numeric method requires generic invocation", )); }; - let argument = arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined); + let argument = match arguments.readable.first() { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + }; let value = match kind { - NumericKind::BigIntAsN(_) => arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant("BigInt width argv was not padded"))?, + NumericKind::BigIntAsN(_) => runtime.root_value( + arguments + .readable + .get(1) + .ok_or(RuntimeError::Invariant("BigInt width argv was not padded"))?, + )?, _ => { let brand = match kind { NumericKind::ToString(kind) => kind, _ => PrimitiveKind::Number, }; - match runtime.primitive_this_value(realm, brand, this_value.clone())? { + let this_value = runtime.root_value(this_value)?; + match runtime.primitive_this_value(realm, brand, this_value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } } @@ -126,7 +135,7 @@ impl NumericStep { resume.format(runtime, 0) } _ => Ok(Self::Number { - value: argument, + value: runtime.unroot_value(&argument)?, resume, }), } @@ -141,7 +150,9 @@ impl NumericResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(NumericStep::Complete(Completion::Throw(value))); + return Ok(NumericStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -149,7 +160,7 @@ impl NumericResume { let radix = crate::engine::value::number::to_int32_sat(value); if !(2..=36).contains(&radix) { return Ok(NumericStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "radix must be between 2 and 36", @@ -173,13 +184,15 @@ impl NumericResume { self.0.bits = match runtime.index_from_number(self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(NumericStep::Complete(Completion::Throw(value))); + return Ok(NumericStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.phase = Phase::BigInt; let value = std::mem::replace(&mut self.0.value, Value::Undefined); Ok(NumericStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: self, }) } @@ -224,13 +237,15 @@ impl NumericResume { )); } let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(NumericStep::Complete(Completion::Throw(value))), }; let value = match runtime.bigint_from_primitive(self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(NumericStep::Complete(Completion::Throw(value))); + return Ok(NumericStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let NumericKind::BigIntAsN(kind) = self.0.kind else { @@ -241,8 +256,8 @@ impl NumericResume { BigIntAsNKind::AsIntN => value.as_int_n(self.0.bits), }; Ok(NumericStep::Complete(match result { - Ok(value) => Completion::Return(Value::BigInt(value)), - Err(_) => Completion::Throw(runtime.new_native_error( + Ok(value) => Completion::Return(runtime.unroot_value(&Value::BigInt(value))?), + Err(_) => Completion::Throw(runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "BigInt is too large to allocate", @@ -259,11 +274,16 @@ pub(crate) fn finish( step = match step { NumericStep::Complete(result) => return Ok(result), NumericStep::Number { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } NumericStep::Primitive { value, resume } => resume.primitive( runtime, - runtime.to_primitive(realm, value, crate::engine::vm::ToPrimitiveHint::Number)?, + runtime.to_primitive( + realm, + runtime.root_and_release_jsvalue(value)?, + crate::engine::vm::ToPrimitiveHint::Number, + )?, )?, }; } diff --git a/src/engine/builtins/primitive/text.rs b/src/engine/builtins/primitive/text.rs index 53400da0..a981c30c 100644 --- a/src/engine/builtins/primitive/text.rs +++ b/src/engine/builtins/primitive/text.rs @@ -9,7 +9,7 @@ use crate::engine::{ }, builtins::native::{StringCharAtKind, StringWellFormedKind}, heap::ContextId, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -40,11 +40,11 @@ impl ScalarTextKind { pub(crate) enum ScalarTextStep { Complete(Completion), String { - value: Value, + value: JsValue, resume: ScalarTextResume, }, Number { - value: Value, + value: JsValue, resume: ScalarTextResume, }, } @@ -71,7 +71,7 @@ pub(crate) struct ScalarTextResumeState { kind: ScalarTextKind, phase: Phase, string: JsString, - arguments: std::vec::IntoIter, + arguments: std::vec::IntoIter, } impl ScalarTextStep { pub(crate) fn start( @@ -86,9 +86,9 @@ impl ScalarTextStep { "String scalar method requires generic invocation", )); }; - if matches!(this_value, Value::Null | Value::Undefined) { + if matches!(this_value, JsValue::Null | JsValue::Undefined) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "null or undefined are forbidden", @@ -96,17 +96,24 @@ impl ScalarTextStep { ))); } let arguments = match kind { - ScalarTextKind::Concat => arguments.readable[..arguments.actual_arg_count].to_vec(), - _ => vec![ - arguments - .readable - .first() - .cloned() - .unwrap_or(Value::Undefined), - ], + ScalarTextKind::Concat => { + let mut values = Vec::new(); + values + .try_reserve_exact(arguments.actual_arg_count) + .map_err(|_| RuntimeError::Invariant("String concat argv allocation failed"))?; + for value in &arguments.readable[..arguments.actual_arg_count] { + values.push(runtime.dup_jsvalue(value)?); + } + values + } + _ => vec![match arguments.readable.first() { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, + }], }; + let this_value = runtime.dup_jsvalue(this_value)?; Ok(Self::String { - value: this_value.clone(), + value: this_value, resume: ScalarTextResume(Box::new(ScalarTextResumeState { realm, kind, @@ -126,7 +133,9 @@ impl ScalarTextResume { let string = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ScalarTextStep::Complete(Completion::Throw(value))); + return Ok(ScalarTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -144,36 +153,42 @@ impl ScalarTextResume { ScalarTextKind::WellFormed(kind) => { Ok(ScalarTextStep::Complete(Completion::Return(match kind { StringWellFormedKind::IsWellFormed => { - Value::Bool(self.0.string.is_well_formed()) + JsValue::Bool(self.0.string.is_well_formed()) } StringWellFormedKind::ToWellFormed => { - Value::String(self.0.string.to_well_formed()) + runtime.unroot_value(&Value::String(self.0.string.to_well_formed()))? } }))) } ScalarTextKind::Iterator => Ok(ScalarTextStep::Complete(Completion::Return( - Value::Object(runtime.new_string_iterator(self.0.realm, self.0.string)?), + JsValue::Object( + runtime + .new_string_iterator(self.0.realm, self.0.string)? + .into_handle(), + ), ))), - ScalarTextKind::Concat => self.concat(), + ScalarTextKind::Concat => self.concat(runtime), _ => { self.0.phase = Phase::Index; Ok(ScalarTextStep::Number { - value: self.0.arguments.next().unwrap_or(Value::Undefined), + value: self.0.arguments.next().unwrap_or(JsValue::Undefined), resume: self, }) } } } - fn concat(mut self) -> Result { + fn concat(mut self, runtime: &Runtime) -> Result { loop { match self.0.arguments.next() { None => { - return Ok(ScalarTextStep::Complete(Completion::Return(Value::String( - self.0.string, - )))); + return Ok(ScalarTextStep::Complete(Completion::Return( + runtime.unroot_value(&Value::String(self.0.string))?, + ))); } - Some(Value::String(chunk)) => { - self.0.string = self.0.string.try_concat(&chunk).map_err(Error::from)? + Some(JsValue::String(id)) => { + let chunk = runtime.0.state.borrow().heap.string(id)?.clone(); + runtime.release_jsvalue(JsValue::String(id))?; + self.0.string = self.0.string.try_concat(&chunk).map_err(Error::from)?; } Some(value) => { self.0.phase = Phase::Chunk; @@ -187,6 +202,7 @@ impl ScalarTextResume { } pub(crate) fn number( self, + runtime: &Runtime, result: NativeConversion, ) -> Result { if !matches!(self.0.phase, Phase::Index) { @@ -197,7 +213,9 @@ impl ScalarTextResume { let number = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ScalarTextStep::Complete(Completion::Throw(value))); + return Ok(ScalarTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let mut index = crate::engine::value::number::to_int32_sat(number); @@ -232,7 +250,9 @@ impl ScalarTextResume { .map_or(Value::Undefined, |point| Value::Int(point as i32)), _ => return Err(RuntimeError::Invariant("String scalar index kind mismatch")), }; - Ok(ScalarTextStep::Complete(Completion::Return(value))) + Ok(ScalarTextStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))) } } pub(crate) fn finish( @@ -244,10 +264,12 @@ pub(crate) fn finish( step = match step { ScalarTextStep::Complete(result) => return Ok(result), ScalarTextStep::String { value, resume } => { + let value = runtime.root_and_release_jsvalue(value)?; resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? } ScalarTextStep::Number { value, resume } => { - resume.number(runtime.native_to_number(realm, &value)?)? + let value = runtime.root_and_release_jsvalue(value)?; + resume.number(runtime, runtime.native_to_number(realm, &value)?)? } }; } diff --git a/src/engine/builtins/promise.rs b/src/engine/builtins/promise.rs index e1abe30b..aa8667d8 100644 --- a/src/engine/builtins/promise.rs +++ b/src/engine/builtins/promise.rs @@ -19,7 +19,7 @@ use crate::engine::object::{ PropertyKey, WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{ConstructorRef, NativeArguments, NativeInvocation}; use std::cell::Cell; @@ -449,10 +449,11 @@ impl Runtime { pub(crate) fn new_rejected_default_promise( &self, realm: ContextId, - reason: Value, + reason: JsValue, ) -> Result { let capability = self.new_default_promise_capability(realm)?; let promise = capability.promise.clone(); + let reason = self.root_and_release_jsvalue(reason)?; self.settle_promise(realm, &promise, PromiseState::Rejected, reason)?; Ok(promise) } @@ -479,15 +480,22 @@ impl Runtime { completion: Completion, ) -> Result, RuntimeError> { let promise = match completion { - Completion::Return(Value::Object(promise)) => promise, - Completion::Return(_) => { - return Ok(NativeConversion::Throw(self.new_native_error( - realm, - NativeErrorKind::Type, - "not an object", - )?)); + Completion::Return(value) => { + let value = self.root_and_release_jsvalue(value)?; + let Value::Object(promise) = value else { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Type, + "not an object", + )?)); + }; + promise + } + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); } - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), }; let capture = self .0 @@ -560,7 +568,10 @@ impl Runtime { PromiseNativeKind::Constructor => { self.call_promise_constructor(realm, invocation, arguments) } - PromiseNativeKind::Species => self.call_promise_species(invocation), + PromiseNativeKind::Species => self + .dispatch_borrowed_invocation(invocation, |invocation| { + self.call_promise_species(invocation) + }), PromiseNativeKind::Then => self.call_promise_then(realm, invocation, arguments), PromiseNativeKind::Catch => self.call_promise_catch(realm, invocation, arguments), PromiseNativeKind::Finally => self.call_promise_finally(realm, invocation, arguments), @@ -578,14 +589,14 @@ impl Runtime { fn call_promise_species( &self, - invocation: NativeInvocation, + invocation: &NativeInvocation, ) -> Result { let NativeInvocation::Getter { this_value } = invocation else { return Err(RuntimeError::Invariant( "Promise species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value)) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } fn call_promise_constructor( @@ -594,14 +605,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::start( - self, - realm, - NativeFunctionId::Promise(PromiseNativeKind::Constructor), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::start( + self, + realm, + NativeFunctionId::Promise(PromiseNativeKind::Constructor), + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(crate) fn call_promise_resolving( @@ -611,14 +624,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::start( - self, - realm, - NativeFunctionId::PromiseResolving(target_kind), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::start( + self, + realm, + NativeFunctionId::PromiseResolving(target_kind), + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(crate) fn call_promise_capability_executor( @@ -627,26 +642,20 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Promise capability executor received a constructor invocation", )); }; + invocation.release(self)?; let active = self.active_function()?; - let resolve = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise capability resolve argv was not padded", - ))?; - let reject = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise capability reject argv was not padded", - ))?; + let resolve = self.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise capability resolve argv was not padded"), + )?)?; + let reject = self.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("Promise capability reject argv was not padded"), + )?)?; let raw_resolve = self.raw_property_value(&resolve)?; let raw_reject = self.raw_property_value(&reject)?; let mut state = self.0.state.borrow_mut(); @@ -659,12 +668,12 @@ impl Runtime { drop(state); drop(resolve); drop(reject); - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } Ok(false) => { state.release_atoms(retained)?; drop(state); - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "resolving function already set", @@ -704,6 +713,7 @@ impl Runtime { } }; let raw = self.raw_property_value(&result)?; + let conversion_edge = raw.conversion_node_edge(); // Prepare job-owned roots before detaching the Promise's reactions, // but do not publish the jobs yet. QuickJS exposes the settled state to @@ -724,9 +734,9 @@ impl Runtime { let prepared_jobs = crate::engine::jobs::PreparedJobs::new(self, prepared_jobs); let settlement = (|| -> Result<(), RuntimeError> { let mut state_ref = self.0.state.borrow_mut(); - let retained_atom = if let RawValue::Symbol(atom) = &raw { - state_ref.atoms.retain(*atom)?; - Some(*atom) + let retained_atom = if let RawValue::Symbol(index) = &raw { + state_ref.atoms.retain_index(*index)?; + Some(*index) } else { None }; @@ -736,14 +746,20 @@ impl Runtime { { Ok(cleanup) => cleanup, Err(error) => { - if let Some(atom) = retained_atom { - state_ref.atoms.release(atom)?; + if let Some(index) = retained_atom { + state_ref.atoms.release_index(index)?; } return Err(error.into()); } }; state_ref.apply_cleanup(cleanup) })(); + // The settle transaction retained its own copy edge for a stored + // string/BigInt; on failure nothing was stored. Either way the + // conversion's producer edge is no longer needed. + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } settlement?; if state == PromiseState::Rejected && !was_handled { self.notify_host_promise_rejection_tracker( @@ -784,14 +800,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::start( - self, - realm, - NativeFunctionId::Promise(PromiseNativeKind::Then), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::start( + self, + realm, + NativeFunctionId::Promise(PromiseNativeKind::Then), + invocation, + arguments, + )? + .finish(self, realm) + }) } fn finish_promise_then( @@ -850,7 +868,9 @@ impl Runtime { .borrow_mut() .heap .promise_mark_handled(promise.object_id())?; - Ok(Completion::Return(Value::Object(capability.promise))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(capability.promise))?, + )) } fn call_promise_catch( @@ -859,14 +879,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::start( - self, - realm, - NativeFunctionId::Promise(PromiseNativeKind::Catch), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::start( + self, + realm, + NativeFunctionId::Promise(PromiseNativeKind::Catch), + invocation, + arguments, + )? + .finish(self, realm) + }) } fn call_promise_static_resolve( @@ -881,14 +903,15 @@ impl Runtime { "Promise resolve/reject received a constructor invocation", )); }; - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise resolve/reject argv was not padded", - ))?; - self.promise_static_resolve_core(realm, kind, this_value, argument) + let argument = self.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise resolve/reject argv was not padded"), + )?)?; + self.promise_static_resolve_core( + realm, + kind, + self.root_and_release_jsvalue(this_value)?, + argument, + ) } fn promise_static_resolve_core( @@ -970,7 +993,9 @@ impl Runtime { .finish(self, realm)? { Completion::Return(_) => Ok(NativeConversion::Value(())), - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), + Completion::Throw(value) => Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )), } } diff --git a/src/engine/builtins/promise/all.rs b/src/engine/builtins/promise/all.rs index ab0b5510..d8670c23 100644 --- a/src/engine/builtins/promise/all.rs +++ b/src/engine/builtins/promise/all.rs @@ -23,8 +23,10 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::aggregate(self, realm, kind, &invocation, arguments)? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::aggregate(self, realm, kind, invocation, arguments)? + .finish(self, realm) + }) } pub(super) fn prepare_promise_aggregate_handlers( @@ -118,14 +120,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.prepare_promise_all_resolve_element(realm, invocation, arguments)? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.prepare_promise_all_resolve_element(realm, invocation, arguments)? + .finish(self, realm) + }) } pub(crate) fn prepare_promise_all_resolve_element( &self, realm: ContextId, - invocation: NativeInvocation, + invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { let NativeInvocation::Call { .. } = invocation else { @@ -157,7 +161,7 @@ impl Runtime { }; if already_called.replace(true) { return Ok(operation::PromiseStep::Complete(Completion::Return( - Value::Undefined, + JsValue::Undefined, ))); } @@ -180,15 +184,17 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.prepare_promise_all_settled_element(target_outcome, realm, invocation, arguments)? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.prepare_promise_all_settled_element(target_outcome, realm, invocation, arguments)? + .finish(self, realm) + }) } pub(crate) fn prepare_promise_all_settled_element( &self, target_outcome: PromiseReactionKind, realm: ContextId, - invocation: NativeInvocation, + invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { let NativeInvocation::Call { .. } = invocation else { @@ -226,7 +232,7 @@ impl Runtime { } if already_called.replace(true) { return Ok(operation::PromiseStep::Complete(Completion::Return( - Value::Undefined, + JsValue::Undefined, ))); } @@ -260,14 +266,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.prepare_promise_any_reject_element(realm, invocation, arguments)? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.prepare_promise_any_reject_element(realm, invocation, arguments)? + .finish(self, realm) + }) } pub(crate) fn prepare_promise_any_reject_element( &self, realm: ContextId, - invocation: NativeInvocation, + invocation: &NativeInvocation, arguments: &NativeArguments, ) -> Result { let NativeInvocation::Call { .. } = invocation else { @@ -299,7 +307,7 @@ impl Runtime { }; if already_called.replace(true) { return Ok(operation::PromiseStep::Complete(Completion::Return( - Value::Undefined, + JsValue::Undefined, ))); } @@ -319,13 +327,9 @@ impl Runtime { &self, arguments: &NativeArguments, ) -> Result { - arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise aggregate element argv was not padded", - )) + self.root_value(arguments.readable.first().ok_or(RuntimeError::Invariant( + "Promise aggregate element argv was not padded", + ))?) } fn define_fresh_aggregate_property( @@ -368,7 +372,9 @@ impl Runtime { if let Some(value) = self.define_array_data_property_without_throw(realm, &values, index, value)? { - return Ok(operation::PromiseStep::Complete(Completion::Throw(value))); + return Ok(operation::PromiseStep::Complete(Completion::Throw( + self.into_jsvalue(value)?, + ))); } let count = remaining @@ -380,7 +386,7 @@ impl Runtime { remaining.set(count); if count != 0 { return Ok(operation::PromiseStep::Complete(Completion::Return( - Value::Undefined, + JsValue::Undefined, ))); } diff --git a/src/engine/builtins/promise/convenience.rs b/src/engine/builtins/promise/convenience.rs index bafb3d4f..8b1ab38d 100644 --- a/src/engine/builtins/promise/convenience.rs +++ b/src/engine/builtins/promise/convenience.rs @@ -14,17 +14,19 @@ impl Runtime { realm: ContextId, invocation: NativeInvocation, ) -> Result { - operation::PromiseStep::convenience( - self, - realm, - PromiseNativeKind::WithResolvers, - &invocation, - &NativeArguments { - readable: Vec::new(), - actual_arg_count: 0, - }, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::convenience( + self, + realm, + PromiseNativeKind::WithResolvers, + invocation, + &NativeArguments { + readable: Vec::new(), + actual_arg_count: 0, + }, + )? + .finish(self, realm) + }) } pub(crate) fn call_promise_try( @@ -33,14 +35,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::convenience( - self, - realm, - PromiseNativeKind::Try, - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::convenience( + self, + realm, + PromiseNativeKind::Try, + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(crate) fn call_promise_race( @@ -49,28 +53,31 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::aggregate( - self, - realm, - PromiseNativeKind::Race, - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::aggregate( + self, + realm, + PromiseNativeKind::Race, + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(crate) fn promise_callable( &self, realm: ContextId, - value: Value, + value: &JsValue, ) -> Result, RuntimeError> { - let Value::Object(object) = value else { + let JsValue::Object(id) = value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, "not a function", )?)); }; + let object = ObjectRef::from_borrowed_handle(self.clone(), *id)?; match self.as_callable(&object)? { Some(callable) => Ok(NativeConversion::Value(callable)), None => Ok(NativeConversion::Throw(self.new_native_error( diff --git a/src/engine/builtins/promise/finally.rs b/src/engine/builtins/promise/finally.rs index a480a3e6..e0065fde 100644 --- a/src/engine/builtins/promise/finally.rs +++ b/src/engine/builtins/promise/finally.rs @@ -15,14 +15,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::start( - self, - realm, - NativeFunctionId::Promise(PromiseNativeKind::Finally), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::start( + self, + realm, + NativeFunctionId::Promise(PromiseNativeKind::Finally), + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(super) fn prepare_promise_finally_handlers( @@ -80,14 +82,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - operation::PromiseStep::start( - self, - realm, - NativeFunctionId::PromiseFinallyHandler(kind), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + operation::PromiseStep::start( + self, + realm, + NativeFunctionId::PromiseFinallyHandler(kind), + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(crate) fn call_promise_finally_thunk( @@ -95,11 +99,13 @@ impl Runtime { kind: PromiseReactionKind, invocation: NativeInvocation, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Promise finally thunk received a constructor invocation", )); }; + invocation.release(self)?; let active = self.active_function()?; let internal = self .0 @@ -116,6 +122,7 @@ impl Runtime { )); }; let value = self.root_raw_value(&value)?; + let value = self.into_jsvalue(value)?; Ok(match kind { PromiseReactionKind::Fulfill => Completion::Return(value), PromiseReactionKind::Reject => Completion::Throw(value), diff --git a/src/engine/builtins/promise/operation.rs b/src/engine/builtins/promise/operation.rs index 6c2ede35..a5ef278b 100644 --- a/src/engine/builtins/promise/operation.rs +++ b/src/engine/builtins/promise/operation.rs @@ -10,7 +10,7 @@ mod then; use crate::engine::builtins::native::{NativeFunctionId, PromiseNativeKind, PromiseResolvingKind}; use crate::engine::heap::{ContextId, InternalCallableData, PromiseState}; use crate::engine::object::{CallableRef, ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{ Completion, call::{ConstructorPrototypeSource, NativeArguments, NativeInvocation}, @@ -100,8 +100,14 @@ impl PromiseStep { pub(super) fn ignore_return(realm: ContextId, callable: CallableRef, argument: Value) -> Self { { let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; - let __pending_field_arguments = vec![argument]; + let __pending_field_receiver = JsValue::Undefined; + let __pending_field_arguments = vec![match argument { + Value::Object(object) => JsValue::Object(object.into_handle()), + other => __pending_field_callable + .runtime() + .into_jsvalue(other) + .expect("Promise ignore-return argument conversion"), + }]; let __pending_field_resume = Box::new(PromiseResume { pending_effect: PromiseStepPending::default(), realm, @@ -130,19 +136,20 @@ impl PromiseStep { | PromiseNativeKind::Race), ) => Self::aggregate(runtime, realm, kind, invocation, arguments), NativeFunctionId::PromiseAllResolveElement => { - runtime.prepare_promise_all_resolve_element(realm, invocation.clone(), arguments) + runtime.prepare_promise_all_resolve_element(realm, invocation, arguments) + } + NativeFunctionId::PromiseAllSettledElement(kind) => { + runtime.prepare_promise_all_settled_element(kind, realm, invocation, arguments) } - NativeFunctionId::PromiseAllSettledElement(kind) => runtime - .prepare_promise_all_settled_element(kind, realm, invocation.clone(), arguments), NativeFunctionId::PromiseAnyRejectElement => { - runtime.prepare_promise_any_reject_element(realm, invocation.clone(), arguments) + runtime.prepare_promise_any_reject_element(realm, invocation, arguments) } NativeFunctionId::Promise(PromiseNativeKind::Finally) | NativeFunctionId::PromiseFinallyHandler(_) => { finally::start(runtime, realm, target, invocation, arguments) } NativeFunctionId::PromiseFinallyThunk(kind) => runtime - .call_promise_finally_thunk(kind, invocation.clone()) + .call_promise_finally_thunk(kind, invocation.dup(runtime)?) .map(Self::Complete), NativeFunctionId::Promise( kind @ (PromiseNativeKind::Try | PromiseNativeKind::WithResolvers), @@ -153,23 +160,18 @@ impl PromiseStep { "Promise.prototype.catch received a constructor invocation", )); }; - let handler = - arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise.catch reject argv was not padded", - ))?; + let handler = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise.catch reject argv was not padded"), + )?)?; Ok({ - let __pending_field_receiver = this_value.clone(); + let __pending_field_receiver = runtime.dup_jsvalue(this_value)?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Then)?; let __pending_field_resume = Box::new(PromiseResume { pending_effect: PromiseStepPending::default(), realm, phase: Phase::CatchThen { - receiver: this_value.clone(), + receiver: runtime.root_value(this_value)?, handler, }, }); @@ -195,14 +197,10 @@ impl PromiseStep { runtime, realm, kind, - this_value.clone(), - arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise resolve/reject argv was not padded", - ))?, + runtime.root_value(this_value)?, + runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise resolve/reject argv was not padded"), + )?)?, ) } NativeFunctionId::PromiseResolving(kind) => { @@ -214,12 +212,13 @@ impl PromiseStep { "Promise constructor did not receive a constructor invocation", )); }; - let executor = - runtime.callable_from_value(arguments.readable.first().cloned().ok_or( - RuntimeError::Invariant("Promise executor argv was not padded"), - )?)?; + let executor = runtime.callable_from_value(runtime.root_value( + arguments.readable.first().ok_or(RuntimeError::Invariant( + "Promise executor argv was not padded", + ))?, + )?)?; Ok({ - let __pending_field_new_target = new_target.clone(); + let __pending_field_new_target = runtime.dup_jsvalue(new_target)?; let __pending_field_resume = Box::new(PromiseResume { pending_effect: PromiseStepPending::default(), realm, @@ -228,11 +227,11 @@ impl PromiseStep { Self::request_prototype(__pending_field_new_target, __pending_field_resume) }) } - NativeFunctionId::Promise(PromiseNativeKind::Species) => runtime - .call_promise_species(invocation.clone()) - .map(Self::Complete), + NativeFunctionId::Promise(PromiseNativeKind::Species) => { + runtime.call_promise_species(invocation).map(Self::Complete) + } NativeFunctionId::PromiseCapabilityExecutor => runtime - .call_promise_capability_executor(realm, invocation.clone(), arguments) + .call_promise_capability_executor(realm, invocation.dup(runtime)?, arguments) .map(Self::Complete), _ => Err(RuntimeError::Invariant("unregistered Promise operation")), } @@ -276,15 +275,11 @@ impl PromiseStep { )); } if already_resolved.replace(true) { - return Ok(Self::Complete(Completion::Return(Value::Undefined))); + return Ok(Self::Complete(Completion::Return(JsValue::Undefined))); } - let resolution = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise resolving argv was not padded", - ))?; + let resolution = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise resolving argv was not padded"), + )?)?; let promise = ObjectRef::from_borrowed_handle(runtime.clone(), promise)?; if kind == PromiseResolvingKind::Reject { runtime.settle_promise(realm, &promise, PromiseState::Rejected, resolution)?; @@ -298,7 +293,7 @@ impl PromiseStep { runtime.settle_promise(realm, &promise, PromiseState::Rejected, reason)?; } else { return Ok({ - let __pending_field_receiver = Value::Object(object.clone()); + let __pending_field_receiver = JsValue::Object(object.clone().into_handle()); let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Then)?; let __pending_field_resume = Box::new(PromiseResume { @@ -319,7 +314,7 @@ impl PromiseStep { } else { runtime.settle_promise(realm, &promise, PromiseState::Fulfilled, resolution)?; } - Ok(Self::Complete(Completion::Return(Value::Undefined))) + Ok(Self::Complete(Completion::Return(JsValue::Undefined))) } pub(crate) fn finish( @@ -351,7 +346,9 @@ impl PromiseResume { }; let prototype = match result { NativeConversion::Throw(value) => { - return Ok(PromiseStep::Complete(Completion::Throw(value))); + return Ok(PromiseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(ConstructorPrototypeSource::Explicit(object)) => object, NativeConversion::Value(ConstructorPrototypeSource::Realm(realm)) => { @@ -364,12 +361,12 @@ impl PromiseResume { let promise = runtime.new_promise_object(&prototype)?; let (resolve, reject) = runtime.create_promise_resolving_functions(self.realm, &promise)?; let arguments = vec![ - Value::Object(resolve.as_object().clone()), - Value::Object(reject.as_object().clone()), + JsValue::Object(resolve.as_object().clone().into_handle()), + JsValue::Object(reject.as_object().clone().into_handle()), ]; Ok({ let __pending_field_callable = executor; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = arguments; let __pending_field_resume = Box::new(Self { pending_effect: PromiseStepPending::default(), @@ -401,7 +398,9 @@ impl PromiseResume { Phase::ConvenienceCapability { .. } => Err(RuntimeError::Invariant( "Promise convenience expected capability", )), - Phase::TryCallback(capability) => convenience::settle(realm, capability, completion), + Phase::TryCallback(capability) => { + convenience::settle(runtime, realm, capability, completion) + } Phase::Finally(phase) => finally::resume(runtime, realm, phase, completion), Phase::InvokeThen { receiver, @@ -413,14 +412,17 @@ impl PromiseResume { return Ok(PromiseStep::Complete(Completion::Throw(value))); } }; - match runtime.promise_callable(realm, value)? { - NativeConversion::Throw(value) => { - Ok(PromiseStep::Complete(Completion::Throw(value))) - } + match runtime.promise_callable(realm, &value)? { + NativeConversion::Throw(value) => Ok(PromiseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), NativeConversion::Value(callable) => Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = receiver; - let __pending_field_arguments = arguments; + let __pending_field_receiver = runtime.into_jsvalue(receiver)?; + let __pending_field_arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; let __pending_field_resume = Box::new(Self { pending_effect: PromiseStepPending::default(), realm, @@ -440,7 +442,7 @@ impl PromiseResume { )), Phase::Aggregate(phase) => aggregate::resume(runtime, realm, phase, completion), Phase::IgnoreReturn => Ok(PromiseStep::Complete(match completion { - Completion::Return(_) => Completion::Return(Value::Undefined), + Completion::Return(_) => Completion::Return(JsValue::Undefined), other => other, })), Phase::Identity => Ok(PromiseStep::Complete(completion)), @@ -451,8 +453,8 @@ impl PromiseResume { } Completion::Return(value) => value, }; - let callable = if let Value::Object(object) = method { - runtime.as_callable(&object)? + let callable = if let JsValue::Object(id) = method { + runtime.as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), id)?)? } else { None }; @@ -461,8 +463,9 @@ impl PromiseResume { }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = receiver; - let __pending_field_arguments = vec![Value::Undefined, handler]; + let __pending_field_receiver = runtime.into_jsvalue(receiver)?; + let __pending_field_arguments = + vec![JsValue::Undefined, runtime.into_jsvalue(handler)?]; let __pending_field_resume = Box::new(Self { pending_effect: PromiseStepPending::default(), realm, @@ -480,7 +483,7 @@ impl PromiseResume { Completion::Return(value) => Ok(PromiseStep::Complete(Completion::Return(value))), Completion::Throw(reason) => Ok({ let __pending_field_callable = reject; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = vec![reason]; let __pending_field_resume = Box::new(Self { pending_effect: PromiseStepPending::default(), @@ -497,7 +500,9 @@ impl PromiseResume { }, Phase::Reaction(targets) => { let Some(targets) = targets else { - return Ok(PromiseStep::Complete(Completion::Return(Value::Undefined))); + return Ok(PromiseStep::Complete(Completion::Return( + JsValue::Undefined, + ))); }; let (target, value) = match completion { Completion::Return(value) => (targets.resolve, value), @@ -510,7 +515,7 @@ impl PromiseResume { ))?; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = vec![value]; let __pending_field_resume = Box::new(Self { pending_effect: PromiseStepPending::default(), @@ -548,12 +553,18 @@ impl PromiseResume { resolution, } => { match completion { - Completion::Throw(reason) => { - runtime.settle_promise(realm, &promise, PromiseState::Rejected, reason)? - } + Completion::Throw(reason) => runtime.settle_promise( + realm, + &promise, + PromiseState::Rejected, + runtime.root_and_release_jsvalue(reason)?, + )?, Completion::Return(then) => { - let then = if let Value::Object(object) = then { - runtime.as_callable(&object)? + let then = if let JsValue::Object(id) = then { + runtime.as_callable(&ObjectRef::from_borrowed_handle( + runtime.clone(), + id, + )?)? } else { None }; @@ -574,13 +585,15 @@ impl PromiseResume { } } } - Ok(PromiseStep::Complete(Completion::Return(Value::Undefined))) + Ok(PromiseStep::Complete(Completion::Return( + JsValue::Undefined, + ))) } Phase::ConstructorExecutor { capability } => { if let Completion::Throw(reason) = completion { return Ok({ let __pending_field_callable = capability.reject; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = vec![reason]; let __pending_field_resume = Box::new(Self { pending_effect: PromiseStepPending::default(), @@ -595,13 +608,15 @@ impl PromiseResume { ) }); } - Ok(PromiseStep::Complete(Completion::Return(Value::Object( - capability.promise, - )))) + Ok(PromiseStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(capability.promise))?, + ))) } Phase::ReturnPromise(promise) => Ok(PromiseStep::Complete(match completion { Completion::Throw(value) => Completion::Throw(value), - Completion::Return(_) => Completion::Return(Value::Object(promise)), + Completion::Return(_) => { + Completion::Return(runtime.into_jsvalue(Value::Object(promise))?) + } })), Phase::ConstructorPrototype { .. } => Err(RuntimeError::Invariant( "Promise constructor expected prototype source", @@ -628,23 +643,23 @@ impl PromiseResume { #[derive(Default)] struct PromiseStepPending { next_iterator: Option, - next_method: Option, + next_method: Option, close_iterator: Option, close_completion: Option, nested_step: Option>, - read_receiver: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, construct_target: Option, - construct_arguments: Option>, - prototype_new_target: Option, + construct_arguments: Option>, + prototype_new_target: Option, } impl PromiseStep { pub(crate) fn request_next( iterator: ObjectRef, - method: Value, + method: JsValue, mut resume: Box, ) -> Self { resume.pending_effect.next_iterator = Some(iterator); @@ -665,7 +680,7 @@ impl PromiseStep { Self::Nested { resume } } pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: Box, ) -> Self { @@ -675,8 +690,8 @@ impl PromiseStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: Box, ) -> Self { resume.pending_effect.call_callable = Some(callable); @@ -686,14 +701,14 @@ impl PromiseStep { } pub(crate) fn request_construct( target: crate::engine::vm::call::ConstructorRef, - arguments: Vec, + arguments: Vec, mut resume: Box, ) -> Self { resume.pending_effect.construct_target = Some(target); resume.pending_effect.construct_arguments = Some(arguments); Self::Construct { resume } } - pub(crate) fn request_prototype(new_target: Value, mut resume: Box) -> Self { + pub(crate) fn request_prototype(new_target: JsValue, mut resume: Box) -> Self { resume.pending_effect.prototype_new_target = Some(new_target); Self::Prototype { resume } } @@ -705,7 +720,7 @@ impl PromiseResume { .take() .expect("PromiseStep Next iterator") } - pub(crate) fn take_next_method(&mut self) -> Value { + pub(crate) fn take_next_method(&mut self) -> JsValue { self.pending_effect .next_method .take() @@ -729,7 +744,7 @@ impl PromiseResume { .take() .expect("PromiseStep Nested step") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.pending_effect .read_receiver .take() @@ -747,13 +762,13 @@ impl PromiseResume { .take() .expect("PromiseStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.pending_effect .call_receiver .take() .expect("PromiseStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.pending_effect .call_arguments .take() @@ -765,13 +780,13 @@ impl PromiseResume { .take() .expect("PromiseStep Construct target") } - pub(crate) fn take_construct_arguments(&mut self) -> Vec { + pub(crate) fn take_construct_arguments(&mut self) -> Vec { self.pending_effect .construct_arguments .take() .expect("PromiseStep Construct arguments") } - pub(crate) fn take_prototype_new_target(&mut self) -> Value { + pub(crate) fn take_prototype_new_target(&mut self) -> JsValue { self.pending_effect .prototype_new_target .take() diff --git a/src/engine/builtins/promise/operation/aggregate.rs b/src/engine/builtins/promise/operation/aggregate.rs index 33654177..538229e9 100644 --- a/src/engine/builtins/promise/operation/aggregate.rs +++ b/src/engine/builtins/promise/operation/aggregate.rs @@ -4,7 +4,7 @@ use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::builtins::{native::PromiseNativeKind, promise::RootedPromiseCapability}; use crate::engine::heap::ContextId; use crate::engine::object::{CallableRef, ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -47,7 +47,7 @@ pub(super) struct Loop { capability: RootedPromiseCapability, resolve: CallableRef, iterator: ObjectRef, - method: Value, + method: JsValue, aggregate: Option, } struct Elements { @@ -63,11 +63,12 @@ fn continuation(realm: ContextId, phase: Phase) -> Box { }) } fn reject( + runtime: &Runtime, realm: ContextId, capability: RootedPromiseCapability, - reason: Value, + reason: JsValue, ) -> Result { - super::convenience::settle(realm, capability, Completion::Throw(reason)) + super::convenience::settle(runtime, realm, capability, Completion::Throw(reason)) } impl PromiseStep { pub(in crate::engine::builtins::promise) fn aggregate( @@ -93,22 +94,22 @@ impl PromiseStep { "Promise aggregate received constructor invocation", )); }; - let Value::Object(object) = this_value else { + let JsValue::Object(object_id) = this_value else { return capability::error(runtime, realm, "not an object"); }; - let constructor = match runtime - .constructor_from_value(realm, Value::Object(object.clone()))? - { - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - NativeConversion::Value(constructor) => constructor, - }; - let iterable = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise aggregate iterable argv was not padded", - ))?; + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *object_id)?; + let constructor = + match runtime.constructor_from_value(realm, Value::Object(object.clone()))? { + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + NativeConversion::Value(constructor) => constructor, + }; + let iterable = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise aggregate iterable argv was not padded"), + )?)?; Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), realm, @@ -130,7 +131,7 @@ pub(super) fn ready( capability: RootedPromiseCapability, ) -> Result { Ok({ - let __pending_field_receiver = Value::Object(constructor.clone()); + let __pending_field_receiver = JsValue::Object(constructor.clone().into_handle()); let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Resolve)?; let __pending_field_resume = continuation( @@ -163,10 +164,12 @@ pub(super) fn resume( Phase::Resolve(state) | Phase::Method { state, .. } | Phase::Iterator { state, .. } - | Phase::NextMethod { state, .. } => reject(realm, state.capability, reason), + | Phase::NextMethod { state, .. } => { + reject(runtime, realm, state.capability, reason) + } Phase::Resolved(state) | Phase::Then(state) => state.close(realm, reason), Phase::Terminal(capability) | Phase::Closed(capability) => { - reject(realm, capability, reason) + reject(runtime, realm, capability, reason) } Phase::Next(_) => Err(RuntimeError::Invariant( "Promise next expected iterator reply", @@ -175,10 +178,15 @@ pub(super) fn resume( } }; match phase { - Phase::Resolve(state) => match runtime.promise_callable(realm, value)? { - NativeConversion::Throw(reason) => reject(realm, state.capability, reason), + Phase::Resolve(state) => match runtime.promise_callable(realm, &value)? { + NativeConversion::Throw(reason) => reject( + runtime, + realm, + state.capability, + runtime.into_jsvalue(reason)?, + ), NativeConversion::Value(resolve) => Ok({ - let __pending_field_receiver = state.iterable.clone(); + let __pending_field_receiver = runtime.into_jsvalue(state.iterable.clone())?; let __pending_field_key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)); let __pending_field_resume = continuation(realm, Phase::Method { state, resolve }); @@ -189,18 +197,23 @@ pub(super) fn resume( ) }), }, - Phase::Method { state, resolve } => match runtime.promise_callable(realm, value)? { + Phase::Method { state, resolve } => match runtime.promise_callable(realm, &value)? { NativeConversion::Throw(_) => { let reason = runtime.new_native_error( realm, NativeErrorKind::Type, "value is not iterable", )?; - reject(realm, state.capability, reason) + reject( + runtime, + realm, + state.capability, + runtime.into_jsvalue(reason)?, + ) } NativeConversion::Value(callable) => Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = state.iterable.clone(); + let __pending_field_receiver = runtime.into_jsvalue(state.iterable.clone())?; let __pending_field_arguments = Vec::new(); let __pending_field_resume = continuation(realm, Phase::Iterator { state, resolve }); @@ -213,13 +226,19 @@ pub(super) fn resume( }), }, Phase::Iterator { state, resolve } => { - let Value::Object(iterator) = value else { + let JsValue::Object(iterator_id) = value else { let reason = runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?; - return reject(realm, state.capability, reason); + return reject( + runtime, + realm, + state.capability, + runtime.into_jsvalue(reason)?, + ); }; + let iterator = ObjectRef::from_borrowed_handle(runtime.clone(), iterator_id)?; Ok({ - let __pending_field_receiver = Value::Object(iterator.clone()); + let __pending_field_receiver = JsValue::Object(iterator.clone().into_handle()); let __pending_field_key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?; let __pending_field_resume = continuation( @@ -273,7 +292,9 @@ pub(super) fn resume( elements.index, )? { NativeConversion::Value(handlers) => handlers, - NativeConversion::Throw(reason) => return state.close(realm, reason), + NativeConversion::Throw(reason) => { + return state.close(realm, runtime.into_jsvalue(reason)?); + } }; let Some(count) = elements.remaining.get().checked_add(1) else { return state.overflow(runtime, realm); @@ -287,8 +308,12 @@ pub(super) fn resume( ] }; Ok({ - let __pending_field_step = - Box::new(PromiseStep::invoke_then(runtime, realm, value, arguments)?); + let __pending_field_step = Box::new(PromiseStep::invoke_then( + runtime, + realm, + runtime.root_and_release_jsvalue(value)?, + arguments, + )?); let __pending_field_resume = continuation(realm, Phase::Then(state)); PromiseStep::request_nested(__pending_field_step, __pending_field_resume) }) @@ -307,7 +332,7 @@ pub(super) fn resume( Ok(state.advance(realm)) } Phase::Terminal(capability) => Ok(PromiseStep::Complete(Completion::Return( - Value::Object(capability.promise), + runtime.into_jsvalue(Value::Object(capability.promise))?, ))), Phase::Closed(_) | Phase::Next(_) => Err(RuntimeError::Invariant( "Promise aggregate unexpected reply", @@ -315,10 +340,10 @@ pub(super) fn resume( } } impl Loop { - fn advance(self: Box, realm: ContextId) -> PromiseStep { + fn advance(mut self: Box, realm: ContextId) -> PromiseStep { { let __pending_field_iterator = self.iterator.clone(); - let __pending_field_method = self.method.clone(); + let __pending_field_method = std::mem::replace(&mut self.method, JsValue::Undefined); let __pending_field_resume = continuation(realm, Phase::Next(self)); PromiseStep::request_next( __pending_field_iterator, @@ -332,7 +357,7 @@ impl Loop { fn close( self: Box, realm: ContextId, - reason: Value, + reason: JsValue, ) -> Result { Ok({ let __pending_field_iterator = self.iterator; @@ -355,7 +380,7 @@ impl Loop { NativeErrorKind::Range, "too many Promise aggregate elements", )?; - self.close(realm, reason) + self.close(realm, runtime.into_jsvalue(reason)?) } pub(super) fn next( self: Box, @@ -364,10 +389,11 @@ impl Loop { result: ObjectIteratorStep, ) -> Result { match result { - ObjectIteratorStep::Throw(reason) => reject(realm, self.capability, reason), + ObjectIteratorStep::Throw(reason) => reject(runtime, realm, self.capability, reason), ObjectIteratorStep::Yield(value) => Ok({ let __pending_field_callable = self.resolve.clone(); - let __pending_field_receiver = Value::Object(self.constructor.clone()); + let __pending_field_receiver = + JsValue::Object(self.constructor.clone().into_handle()); let __pending_field_arguments = vec![value]; let __pending_field_resume = continuation(realm, Phase::Resolved(self)); PromiseStep::request_call( @@ -392,20 +418,24 @@ impl Loop { let (callable, value) = if self.kind == PromiseNativeKind::Any { ( self.capability.reject.clone(), - Value::Object(runtime.new_internal_aggregate_error( - realm, - elements.values.clone(), - )?), + JsValue::Object( + runtime + .new_internal_aggregate_error( + realm, + elements.values.clone(), + )? + .into_handle(), + ), ) } else { ( self.capability.resolve.clone(), - Value::Object(elements.values.clone()), + JsValue::Object(elements.values.clone().into_handle()), ) }; return Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = vec![value]; let __pending_field_resume = continuation(realm, Phase::Terminal(self.capability)); @@ -418,9 +448,9 @@ impl Loop { }); } } - Ok(PromiseStep::Complete(Completion::Return(Value::Object( - self.capability.promise, - )))) + Ok(PromiseStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.capability.promise))?, + ))) } } } diff --git a/src/engine/builtins/promise/operation/capability.rs b/src/engine/builtins/promise/operation/capability.rs index 5281cf28..312934a7 100644 --- a/src/engine/builtins/promise/operation/capability.rs +++ b/src/engine/builtins/promise/operation/capability.rs @@ -1,7 +1,7 @@ //! Capability construction roots its executor until the constructor reply is validated. use super::{ - Completion, ContextId, NativeConversion, Phase, PromiseResume, PromiseStep, - RootedPromiseCapability, Runtime, RuntimeError, Value, + Completion, ContextId, JsValue, NativeConversion, Phase, PromiseResume, PromiseStep, + RootedPromiseCapability, Runtime, RuntimeError, }; use crate::engine::vm::call::ConstructorRef; @@ -18,7 +18,8 @@ impl PromiseResume { let executor = runtime.prepare_promise_capability_executor(self.realm)?; Ok({ let __pending_field_target = target; - let __pending_field_arguments = vec![Value::Object(executor.as_object().clone())]; + let __pending_field_arguments = + vec![JsValue::Object(executor.as_object().clone().into_handle())]; let __pending_field_resume = Box::new(Self { pending_effect: super::PromiseStepPending::default(), realm: self.realm, @@ -42,7 +43,9 @@ impl PromiseResume { ) -> Result { let capability = match result { NativeConversion::Throw(value) => { - return Ok(PromiseStep::Complete(Completion::Throw(value))); + return Ok(PromiseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(capability) => capability, }; @@ -70,8 +73,8 @@ impl PromiseResume { }; Ok({ let __pending_field_callable = target; - let __pending_field_receiver = Value::Undefined; - let __pending_field_arguments = vec![argument]; + let __pending_field_receiver = JsValue::Undefined; + let __pending_field_arguments = vec![runtime.into_jsvalue(argument)?]; let __pending_field_resume = Box::new(Self { pending_effect: super::PromiseStepPending::default(), realm: self.realm, @@ -101,7 +104,7 @@ pub(super) fn error( message: &'static str, ) -> Result { Ok(PromiseStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, crate::engine::api::error::NativeErrorKind::Type, message, diff --git a/src/engine/builtins/promise/operation/convenience.rs b/src/engine/builtins/promise/operation/convenience.rs index c5c90c6e..c9f1ce23 100644 --- a/src/engine/builtins/promise/operation/convenience.rs +++ b/src/engine/builtins/promise/operation/convenience.rs @@ -3,8 +3,8 @@ use super::{Phase, PromiseResume, PromiseStep, capability}; use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::builtins::{native::PromiseNativeKind, promise::RootedPromiseCapability}; use crate::engine::heap::ContextId; -use crate::engine::object::{DescriptorField, OrdinaryPropertyDescriptor}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -23,22 +23,30 @@ impl PromiseStep { "Promise convenience received constructor invocation", )); }; - let Value::Object(object) = this_value else { + let JsValue::Object(object_id) = this_value else { return capability::error(runtime, realm, "not an object"); }; - let constructor = match runtime - .constructor_from_value(realm, Value::Object(object.clone()))? - { - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), - NativeConversion::Value(constructor) => constructor, - }; + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *object_id)?; + let constructor = + match runtime.constructor_from_value(realm, Value::Object(object.clone()))? { + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + NativeConversion::Value(constructor) => constructor, + }; Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), realm, phase: Phase::ConvenienceCapability { kind, arguments: NativeArguments { - readable: arguments.readable.clone(), + readable: arguments + .readable + .iter() + .map(|value| runtime.dup_jsvalue(value)) + .collect::, _>>()?, actual_arg_count: arguments.actual_arg_count, }, }, @@ -83,24 +91,28 @@ pub(super) fn ready( )); } } - return Ok(PromiseStep::Complete(Completion::Return(Value::Object( - result, - )))); + return Ok(PromiseStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(result))?, + ))); } - let callback = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise.try callback argv was not padded", - ))?; + let callback = arguments.readable.first().ok_or(RuntimeError::Invariant( + "Promise.try callback argv was not padded", + ))?; match runtime.promise_callable(realm, callback)? { - NativeConversion::Throw(reason) => settle(realm, capability, Completion::Throw(reason)), + NativeConversion::Throw(reason) => settle( + runtime, + realm, + capability, + Completion::Throw(runtime.into_jsvalue(reason)?), + ), NativeConversion::Value(callable) => Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; - let __pending_field_arguments = - arguments.readable[1..arguments.actual_arg_count.max(1)].to_vec(); + let __pending_field_receiver = JsValue::Undefined; + let __pending_field_arguments = arguments.readable + [1..arguments.actual_arg_count.max(1)] + .iter() + .map(|value| runtime.dup_jsvalue(value)) + .collect::, _>>()?; let __pending_field_resume = Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), realm, @@ -117,6 +129,7 @@ pub(super) fn ready( } pub(super) fn settle( + _runtime: &Runtime, realm: ContextId, capability: RootedPromiseCapability, completion: Completion, @@ -127,7 +140,7 @@ pub(super) fn settle( }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = vec![value]; let __pending_field_resume = Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), diff --git a/src/engine/builtins/promise/operation/finally.rs b/src/engine/builtins/promise/operation/finally.rs index 337a4587..9c592231 100644 --- a/src/engine/builtins/promise/operation/finally.rs +++ b/src/engine/builtins/promise/operation/finally.rs @@ -6,7 +6,7 @@ use crate::engine::heap::PromiseReactionKind; use crate::engine::heap::{ContextId, InternalCallableData}; use crate::engine::object::WellKnownSymbol; use crate::engine::object::{ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -58,7 +58,7 @@ impl PromiseStep { }, }); Self::request_read( - __pending_field_receiver, + runtime.into_jsvalue(__pending_field_receiver)?, __pending_field_key, __pending_field_resume, ) @@ -77,19 +77,16 @@ pub(super) fn start( "Promise finally received constructor invocation", )); }; - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise finally argv was not padded", - ))?; + let argument = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("Promise finally argv was not padded"), + )?)?; if target == NativeFunctionId::Promise(PromiseNativeKind::Finally) { - let Value::Object(receiver) = this_value else { + let JsValue::Object(receiver_id) = this_value else { return capability::error(runtime, realm, "not an object"); }; + let receiver = ObjectRef::from_borrowed_handle(runtime.clone(), *receiver_id)?; return Ok({ - let __pending_field_receiver = this_value.clone(); + let __pending_field_receiver = runtime.dup_jsvalue(this_value)?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Constructor)?; let __pending_field_resume = continuation( @@ -141,7 +138,7 @@ pub(super) fn start( }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = Vec::new(); let __pending_field_resume = continuation( realm, @@ -171,9 +168,11 @@ pub(super) fn resume( }; match phase { Phase::Constructor { receiver, callback } => match value { - Value::Undefined => handlers(runtime, realm, receiver, callback, None), - Value::Object(constructor) => Ok({ - let __pending_field_receiver = Value::Object(constructor); + JsValue::Undefined => handlers(runtime, realm, receiver, callback, None), + JsValue::Object(constructor_id) => Ok({ + let constructor = ObjectRef::from_borrowed_handle(runtime.clone(), constructor_id)?; + runtime.release_jsvalue(JsValue::Object(constructor_id))?; + let __pending_field_receiver = JsValue::Object(constructor.into_handle()); let __pending_field_key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Species)); let __pending_field_resume = @@ -184,14 +183,21 @@ pub(super) fn resume( __pending_field_resume, ) }), - _ => capability::error(runtime, realm, "not an object"), + value => { + runtime.release_jsvalue(value)?; + capability::error(runtime, realm, "not an object") + } }, Phase::Species { receiver, callback } => { let constructor = match value { - Value::Undefined | Value::Null => None, - value => match runtime.constructor_from_value(realm, value)? { + JsValue::Undefined | JsValue::Null => None, + value => match runtime + .constructor_from_value(realm, runtime.root_and_release_jsvalue(value)?)? + { NativeConversion::Throw(value) => { - return Ok(PromiseStep::Complete(Completion::Throw(value))); + return Ok(PromiseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(constructor) => Some(constructor), }, @@ -208,25 +214,40 @@ pub(super) fn resume( realm, PromiseNativeKind::Resolve, constructor, - value, + runtime.root_and_release_jsvalue(value)?, )?); let __pending_field_resume = continuation(realm, Phase::Resolved { settlement, kind }); PromiseStep::request_nested(__pending_field_step, __pending_field_resume) }), Phase::Resolved { settlement, kind } => { let raw = runtime.raw_property_value(&settlement)?; - let thunk = runtime.new_internal_promise_function( + // The internal callable retains its own copy edge inside the + // allocation, so the conversion's producer edge is released on + // every exit. + let conversion_edge = raw.conversion_node_edge(); + let thunk = match runtime.new_internal_promise_function( realm, NativeFunctionId::PromiseFinallyThunk(kind), 0, 0, InternalCallableData::PromiseFinallyThunk { value: raw }, - )?; + ) { + Ok(thunk) => thunk, + Err(error) => { + if let Some(edge) = conversion_edge { + runtime.release_converted_node_edge(edge); + } + return Err(error); + } + }; + if let Some(edge) = conversion_edge { + runtime.release_converted_node_edge(edge); + } drop(settlement); PromiseStep::invoke_then( runtime, realm, - value, + runtime.root_and_release_jsvalue(value)?, vec![Value::Object(thunk.as_object().clone())], ) } diff --git a/src/engine/builtins/promise/operation/jobs.rs b/src/engine/builtins/promise/operation/jobs.rs index 68380455..c00d9e2e 100644 --- a/src/engine/builtins/promise/operation/jobs.rs +++ b/src/engine/builtins/promise/operation/jobs.rs @@ -1,7 +1,7 @@ //! FIFO jobs keep their callback and resolution targets rooted across driver turns. use super::{ - Completion, ContextId, ObjectRef, Phase, PromiseResume, PromiseStep, Runtime, RuntimeError, - Value, + Completion, ContextId, JsValue, ObjectRef, Phase, PromiseResume, PromiseStep, Runtime, + RuntimeError, }; use crate::engine::heap::{ObjectId, PromiseReaction, PromiseReactionKind, RawValue}; @@ -25,12 +25,12 @@ impl PromiseStep { ))?; let (resolve, reject) = runtime.create_promise_resolving_functions(realm, &promise)?; let arguments = vec![ - Value::Object(resolve.as_object().clone()), - Value::Object(reject.as_object().clone()), + JsValue::Object(resolve.as_object().clone().into_handle()), + JsValue::Object(reject.as_object().clone().into_handle()), ]; Ok({ let __pending_field_callable = then; - let __pending_field_receiver = Value::Object(thenable); + let __pending_field_receiver = JsValue::Object(thenable.into_handle()); let __pending_field_arguments = arguments; let __pending_field_resume = Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), @@ -76,8 +76,8 @@ impl PromiseStep { ))?; Ok({ let __pending_field_callable = handler; - let __pending_field_receiver = Value::Undefined; - let __pending_field_arguments = vec![argument]; + let __pending_field_receiver = JsValue::Undefined; + let __pending_field_arguments = vec![runtime.into_jsvalue(argument.clone())?]; let __pending_field_resume = resume; Self::request_call( __pending_field_callable, @@ -87,6 +87,7 @@ impl PromiseStep { ) }) } else { + let argument = runtime.into_jsvalue(argument)?; let completion = if reaction.kind == PromiseReactionKind::Reject { Completion::Throw(argument) } else { diff --git a/src/engine/builtins/promise/operation/resolve.rs b/src/engine/builtins/promise/operation/resolve.rs index 8d7aed63..4ef001ac 100644 --- a/src/engine/builtins/promise/operation/resolve.rs +++ b/src/engine/builtins/promise/operation/resolve.rs @@ -1,7 +1,7 @@ //! Static and intrinsic PromiseResolve share the constructor identity fast path. use super::{ - Completion, ContextId, NativeConversion, ObjectRef, Phase, PromiseNativeKind, PromiseResume, - PromiseStep, Runtime, RuntimeError, Value, + Completion, ContextId, JsValue, NativeConversion, ObjectRef, Phase, PromiseNativeKind, + PromiseResume, PromiseStep, Runtime, RuntimeError, Value, }; use crate::engine::heap::ObjectPayload; impl PromiseStep { @@ -29,7 +29,7 @@ impl PromiseStep { ) { return Ok({ - let __pending_field_receiver = Value::Object(promise.clone()); + let __pending_field_receiver = JsValue::Object(promise.clone().into_handle()); let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Constructor)?; let __pending_field_resume = Box::new(PromiseResume { @@ -61,10 +61,16 @@ pub(super) fn constructor( ) -> Result { match completion { Completion::Throw(value) => Ok(PromiseStep::Complete(Completion::Throw(value))), - Completion::Return(value) if value.same_value(&Value::Object(constructor.clone())) => { - Ok(PromiseStep::Complete(Completion::Return(argument))) + Completion::Return(value) => { + let value = runtime.root_and_release_jsvalue(value)?; + if value.same_value(&Value::Object(constructor.clone())) { + Ok(PromiseStep::Complete(Completion::Return( + runtime.into_jsvalue(argument)?, + ))) + } else { + create(runtime, realm, constructor, argument, kind) + } } - Completion::Return(_) => create(runtime, realm, constructor, argument, kind), } } fn create( @@ -76,7 +82,9 @@ fn create( ) -> Result { let constructor = match runtime.constructor_from_value(realm, Value::Object(constructor))? { NativeConversion::Throw(value) => { - return Ok(PromiseStep::Complete(Completion::Throw(value))); + return Ok(PromiseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(constructor) => constructor, }; diff --git a/src/engine/builtins/promise/operation/then.rs b/src/engine/builtins/promise/operation/then.rs index e40afe18..1ad1a76f 100644 --- a/src/engine/builtins/promise/operation/then.rs +++ b/src/engine/builtins/promise/operation/then.rs @@ -1,7 +1,7 @@ //! Public then preserves species/capability effects before inspecting handlers. use super::{ - Completion, ContextId, NativeArguments, NativeConversion, NativeInvocation, ObjectRef, Phase, - PromiseResume, PromiseStep, Runtime, RuntimeError, Value, + Completion, ContextId, JsValue, NativeArguments, NativeConversion, NativeInvocation, ObjectRef, + Phase, PromiseResume, PromiseStep, Runtime, RuntimeError, Value, }; use crate::engine::{ heap::ObjectPayload, @@ -19,9 +19,10 @@ impl PromiseStep { "Promise.prototype.then received a constructor invocation", )); }; - let Value::Object(promise) = this_value else { + let JsValue::Object(promise_id) = this_value else { return super::capability::error(runtime, realm, "not a promise"); }; + let promise = ObjectRef::from_borrowed_handle(runtime.clone(), *promise_id)?; if !matches!( runtime .0 @@ -35,23 +36,15 @@ impl PromiseStep { return super::capability::error(runtime, realm, "not a promise"); } let handlers = ThenHandlers::Public([ - arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise.then fulfill argv was not padded", - ))?, - arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Promise.then reject argv was not padded", - ))?, + runtime.root_value(arguments.readable.first().ok_or(RuntimeError::Invariant( + "Promise.then fulfill argv was not padded", + ))?)?, + runtime.root_value(arguments.readable.get(1).ok_or(RuntimeError::Invariant( + "Promise.then reject argv was not padded", + ))?)?, ]); Ok({ - let __pending_field_receiver = Value::Object(promise.clone()); + let __pending_field_receiver = JsValue::Object(promise.clone().into_handle()); let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Constructor)?; let __pending_field_resume = Box::new(PromiseResume { @@ -79,14 +72,16 @@ pub(super) fn constructor( ) -> Result { match result { Completion::Throw(value) => Ok(PromiseStep::Complete(Completion::Throw(value))), - Completion::Return(Value::Undefined) => Box::new(PromiseResume { + Completion::Return(JsValue::Undefined) => Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), realm, phase: Phase::ThenCapability { promise, handlers }, }) .capability(runtime, None), - Completion::Return(Value::Object(constructor)) => Ok({ - let __pending_field_receiver = Value::Object(constructor); + Completion::Return(JsValue::Object(constructor_id)) => Ok({ + let constructor = ObjectRef::from_borrowed_handle(runtime.clone(), constructor_id)?; + runtime.release_jsvalue(JsValue::Object(constructor_id))?; + let __pending_field_receiver = JsValue::Object(constructor.into_handle()); let __pending_field_key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Species)); let __pending_field_resume = Box::new(PromiseResume { @@ -100,7 +95,10 @@ pub(super) fn constructor( __pending_field_resume, ) }), - Completion::Return(_) => super::capability::error(runtime, realm, "not an object"), + Completion::Return(value) => { + runtime.release_jsvalue(value)?; + super::capability::error(runtime, realm, "not an object") + } } } pub(super) fn species( @@ -112,13 +110,17 @@ pub(super) fn species( ) -> Result { let constructor = match result { Completion::Throw(value) => return Ok(PromiseStep::Complete(Completion::Throw(value))), - Completion::Return(Value::Undefined | Value::Null) => None, - Completion::Return(value) => match runtime.constructor_from_value(realm, value)? { - NativeConversion::Throw(value) => { - return Ok(PromiseStep::Complete(Completion::Throw(value))); + Completion::Return(JsValue::Undefined | JsValue::Null) => None, + Completion::Return(value) => { + match runtime.constructor_from_value(realm, runtime.root_and_release_jsvalue(value)?)? { + NativeConversion::Throw(value) => { + return Ok(PromiseStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } + NativeConversion::Value(constructor) => Some(constructor), } - NativeConversion::Value(constructor) => Some(constructor), - }, + } }; Box::new(PromiseResume { pending_effect: super::PromiseStepPending::default(), @@ -190,7 +192,7 @@ impl PromiseStep { handlers: ThenHandlers, ) -> Result { Ok({ - let __pending_field_receiver = Value::Object(promise.clone()); + let __pending_field_receiver = JsValue::Object(promise.clone().into_handle()); let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Constructor)?; let __pending_field_resume = Box::new(PromiseResume { @@ -255,6 +257,6 @@ impl ThenHandlers { Some(&reject), &capability, )?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } } diff --git a/src/engine/builtins/proxy.rs b/src/engine/builtins/proxy.rs index 40bc6af6..1c3c9ead 100644 --- a/src/engine/builtins/proxy.rs +++ b/src/engine/builtins/proxy.rs @@ -13,12 +13,26 @@ use crate::engine::builtins::native::NativeFunctionId; use crate::engine::heap::{ContextId, InternalCallableData, ObjectData, ObjectPayload}; use crate::engine::object::{DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; +fn runtime_value_at( + runtime: &Runtime, + arguments: &NativeArguments, + index: usize, + message: &'static str, +) -> Result { + runtime.root_value( + arguments + .readable + .get(index) + .ok_or(RuntimeError::Invariant(message))?, + ) +} + impl Runtime { /// Publish `%Proxy%` with QuickJS's exact initial own-property surface. /// @@ -128,24 +142,20 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Construct { .. } = invocation else { + let NativeInvocation::Construct { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Proxy constructor did not receive a constructor invocation", )); }; - let target = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant("Proxy target argv was not padded"))?; - let handler = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant("Proxy handler argv was not padded"))?; + invocation.release(self)?; + let target = runtime_value_at(self, arguments, 0, "Proxy target argv was not padded")?; + let handler = runtime_value_at(self, arguments, 1, "Proxy handler argv was not padded")?; match self.new_proxy(realm, target, handler)? { - NativeConversion::Value(proxy) => Ok(Completion::Return(Value::Object(proxy))), - NativeConversion::Throw(value) => Ok(Completion::Throw(value)), + NativeConversion::Value(proxy) => { + Ok(Completion::Return(JsValue::Object(proxy.into_handle()))) + } + NativeConversion::Throw(value) => Ok(Completion::Throw(self.into_jsvalue(value)?)), } } @@ -156,28 +166,30 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Proxy.revocable did not receive a call invocation", )); }; - let target = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Proxy.revocable target argv was not padded", - ))?; - let handler = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "Proxy.revocable handler argv was not padded", - ))?; + invocation.release(self)?; + let target = runtime_value_at( + self, + arguments, + 0, + "Proxy.revocable target argv was not padded", + )?; + let handler = runtime_value_at( + self, + arguments, + 1, + "Proxy.revocable handler argv was not padded", + )?; let proxy = match self.new_proxy(realm, target, handler)? { NativeConversion::Value(proxy) => proxy, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let revoke = self.new_internal_promise_function( realm, @@ -213,7 +225,7 @@ impl Runtime { )); } } - Ok(Completion::Return(Value::Object(result))) + Ok(Completion::Return(JsValue::Object(result.into_handle()))) } /// Consume a revocation closure's capture exactly once. @@ -221,15 +233,17 @@ impl Runtime { &self, invocation: NativeInvocation, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Proxy revoke function did not receive a call invocation", )); }; + invocation.release(self)?; let active = self.active_function()?; let mut state = self.0.state.borrow_mut(); let (_, cleanup) = state.heap.revoke_proxy_from_callable(active.object_id())?; state.apply_cleanup(cleanup)?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } } diff --git a/src/engine/builtins/qjs_host.rs b/src/engine/builtins/qjs_host.rs index e0407c2d..b8cea677 100644 --- a/src/engine/builtins/qjs_host.rs +++ b/src/engine/builtins/qjs_host.rs @@ -13,7 +13,7 @@ use crate::engine::api::runtime_error::RuntimeError; use crate::engine::builtins::native::NativeFunctionId; use crate::engine::object::{CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor}; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -34,11 +34,13 @@ impl Runtime { )); } }; - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "qjs output helper received a constructor invocation", )); }; + invocation.release(self)?; let mut line = Vec::new(); for (index, argument) in arguments.readable[..arguments.actual_arg_count] @@ -48,7 +50,8 @@ impl Runtime { if index != 0 { line.push(b' '); } - if let Value::String(value) = argument { + if let JsValue::String(id) = argument { + let value = self.0.state.borrow().heap.string(*id)?.clone(); value.try_append_wtf8_bytes(&mut line).map_err(|_| { RuntimeError::Engine(Error::new( ErrorKind::Internal, @@ -64,7 +67,7 @@ impl Runtime { // Upstream deliberately ignores fwrite/putchar/fflush failures. Keep // host I/O outside JavaScript completion semantics for exact parity. self.with_host_callback(|| self.0.host_services.write_output(&line, flush))?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } } diff --git a/src/engine/builtins/qjs_value_printer.rs b/src/engine/builtins/qjs_value_printer.rs index fc22a66c..03f2317f 100644 --- a/src/engine/builtins/qjs_value_printer.rs +++ b/src/engine/builtins/qjs_value_printer.rs @@ -8,7 +8,7 @@ use crate::engine::api::error::{Error, ErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::{Atom, AtomSpelling}; +use crate::engine::atom::{Atom, AtomIdx, AtomSpelling}; use crate::engine::builtins as intrinsics; use crate::engine::builtins::native::PromiseResolvingKind; use crate::engine::code::function::metadata::FunctionKind; @@ -19,7 +19,7 @@ use crate::engine::heap::{ PrimitiveObjectData, PropertySlot, RawValue, RegExpObjectData, }; use crate::engine::object::access::raw_string_property_one_level; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::frames::ActiveCollectionRecord; use std::cell::OnceCell; use std::collections::BTreeSet; @@ -107,7 +107,7 @@ impl Runtime { /// are quoted just like upstream. pub fn qjs_print_value_bytes(&self, value: &Value) -> Result, RuntimeError> { let mut output = Vec::new(); - self.qjs_print_value_into_bytes(value, &mut output)?; + self.print_value_rooted_into_bytes(value, &mut output)?; Ok(output) } @@ -115,6 +115,15 @@ impl Runtime { /// The qjs host uses this to assemble a line without allocating and then /// copying a temporary vector for every non-String argument. pub(crate) fn qjs_print_value_into_bytes( + &self, + value: &JsValue, + output: &mut Vec, + ) -> Result<(), RuntimeError> { + let value = self.root_and_release_jsvalue(self.dup_jsvalue(value)?)?; + self.print_value_rooted_into_bytes(&value, output) + } + + fn print_value_rooted_into_bytes( &self, value: &Value, output: &mut Vec, @@ -134,7 +143,12 @@ impl Runtime { stack_atom: stack.atom(), }; let raw = self.raw_property_value(value)?; - printer.print_raw_value(&raw) + let printed = printer.print_raw_value(&raw); + // Diagnostic rendering never stores the converted value, so release + // the conversion's producer string/BigInt edge immediately. + self.release_converted_value_edge(&raw); + printed?; + Ok(()) } } @@ -148,13 +162,23 @@ impl QjsValuePrinter<'_, '_> { RawValue::Int(value) => self.push_ascii(&value.to_string()), RawValue::Float(value) => self.print_float(*value), RawValue::BigInt(value) => { - self.push_ascii(&value.to_string()); + let text = { + let state = self.runtime.0.state.borrow(); + state.heap.bigint(*value)?.to_string() + }; + self.push_ascii(&text); self.output.push(b'n'); } - RawValue::String(value) => self.print_string(value), - RawValue::Symbol(atom) => { + RawValue::String(value) => { + let string = { + let state = self.runtime.0.state.borrow(); + state.heap.string(*value)?.clone() + }; + self.print_string(&string); + } + RawValue::Symbol(index) => { self.push_ascii("Symbol("); - self.print_atom(*atom)?; + self.print_atom(self.brand_atom(*index)?)?; self.output.push(b')'); } RawValue::Object(object) => self.print_object(*object)?, @@ -258,6 +282,12 @@ impl QjsValuePrinter<'_, '_> { .extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); } + /// Brand one internal atom index arriving on a `RawValue` boundary before + /// resolving its spelling. + fn brand_atom(&self, index: AtomIdx) -> Result { + Ok(self.runtime.0.state.borrow().atoms.brand(index)?) + } + fn print_atom(&mut self, atom: Atom) -> Result<(), RuntimeError> { enum OwnedSpelling { Integer(u32), @@ -718,7 +748,7 @@ impl QjsValuePrinter<'_, '_> { if let Some(fast_len) = arguments_fast_len { if state .atoms - .array_index(entry.atom)? + .array_index(state.atoms.brand(entry.atom)?)? .is_some_and(|index| index < fast_len) { // QuickJS keeps the fast Arguments prefix in shape slots, @@ -744,7 +774,7 @@ impl QjsValuePrinter<'_, '_> { PropertySlot::AutoInit(_) => PrintablePropertyValue::AutoInit, }; properties.push(PrintableProperty { - atom: entry.atom, + atom: state.atoms.brand(entry.atom)?, value, }); } @@ -760,7 +790,7 @@ impl QjsValuePrinter<'_, '_> { ObjectPayload::RawJson => ("Object", PrintableBody::Ordinary), ObjectPayload::Array { dense } => { let length = shape - .find(self.length_atom) + .find(AtomIdx::from_raw(self.length_atom.raw())) .and_then(|index| object_data.slots.get(index as usize)) .and_then(|slot| match slot { PropertySlot::Data(RawValue::Int(value)) => Some(*value as u32), diff --git a/src/engine/builtins/reflect.rs b/src/engine/builtins/reflect.rs index eb22bb02..eb992da7 100644 --- a/src/engine/builtins/reflect.rs +++ b/src/engine/builtins/reflect.rs @@ -17,7 +17,7 @@ use crate::engine::object::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -96,7 +96,8 @@ impl Runtime { ); } for (entry, slot) in shape.entries().iter().zip(&object_data.slots) { - let Some(index) = state.atoms.array_index(entry.atom)? else { + let Some(index) = state.atoms.array_index(state.atoms.brand(entry.atom)?)? + else { continue; }; if index >= expected_len { @@ -316,11 +317,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Reflect method did not receive a generic invocation", )); }; + invocation.release(self)?; match kind { ReflectKind::Apply => self.call_reflect_apply(realm, arguments), ReflectKind::Construct => self.call_reflect_construct(realm, arguments), @@ -355,7 +358,7 @@ impl Runtime { realm, super::function::invoke::InvokeKind::ReflectApply, &NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, arguments, )?, @@ -375,7 +378,7 @@ impl Runtime { realm, super::function::invoke::InvokeKind::ReflectConstruct, &NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, arguments, )?, diff --git a/src/engine/builtins/reflect/tests.rs b/src/engine/builtins/reflect/tests.rs index 889bcaad..40421414 100644 --- a/src/engine/builtins/reflect/tests.rs +++ b/src/engine/builtins/reflect/tests.rs @@ -1,4 +1,5 @@ use crate::engine::api::Context; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeCProto; use super::*; @@ -54,7 +55,8 @@ fn global_reflect_is_realm_aware_lazy_and_complete() { let state = runtime.0.state.borrow(); let object = state.heap.object(global.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -107,7 +109,12 @@ fn global_reflect_is_realm_aware_lazy_and_complete() { let state = runtime.0.state.borrow(); let object = state.heap.object(first_reflect.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(method_key.atom()).unwrap()).unwrap(); + let slot_index = usize::try_from( + shape + .find(AtomIdx::from_raw(method_key.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), diff --git a/src/engine/builtins/regexp/compile.rs b/src/engine/builtins/regexp/compile.rs index d51fbb1f..b9176a75 100644 --- a/src/engine/builtins/regexp/compile.rs +++ b/src/engine/builtins/regexp/compile.rs @@ -7,7 +7,7 @@ use crate::engine::api::runtime_error::RuntimeError; use crate::engine::heap::{ContextId, RegExpObjectData}; use crate::engine::object::ObjectRef; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; use crate::engine::vm::{Completion, ToPrimitiveHint}; use crate::regexp::CompiledRegExp; @@ -32,8 +32,8 @@ impl Runtime { step = match step { RegExpCompileStep::Complete(result) => return Ok(result), RegExpCompileStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - self.to_primitive(realm, value, ToPrimitiveHint::String)? + let result = if matches!(value, JsValue::Object(_)) { + self.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? } else { Completion::Return(value) }; @@ -64,16 +64,18 @@ impl Runtime { if let Some(value) = self.set_property_or_throw(realm, regexp, &last_index, Value::Int(0))? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } - Ok(Completion::Return(Value::Object(regexp.clone()))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(regexp.clone()))?, + )) } } pub(crate) enum RegExpCompileStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, resume: RegExpCompileResume, }, } @@ -112,9 +114,14 @@ impl RegExpCompileStep { "RegExp.prototype.compile did not receive a generic invocation", )); }; - let Some(_) = runtime.genuine_regexp(this_value)? else { + let this_value = runtime.root_value(this_value)?; + let Some(_) = runtime.genuine_regexp(&this_value)? else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "RegExp object expected")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "RegExp object expected", + )?, ))); }; let Value::Object(regexp) = this_value else { @@ -122,16 +129,16 @@ impl RegExpCompileStep { "genuine RegExp snapshot accepted a primitive receiver", )); }; - let pattern = arguments.readable.first().ok_or(RuntimeError::Invariant( - "RegExp compile pattern argv was not padded", - ))?; - let flags = arguments.readable.get(1).ok_or(RuntimeError::Invariant( - "RegExp compile flags argv was not padded", - ))?; - if let Some(genuine) = runtime.genuine_regexp(pattern)? { + let pattern = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("RegExp compile pattern argv was not padded"), + )?)?; + let flags = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("RegExp compile flags argv was not padded"), + )?)?; + if let Some(genuine) = runtime.genuine_regexp(&pattern)? { if !matches!(flags, Value::Undefined) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "flags must be undefined", @@ -140,22 +147,22 @@ impl RegExpCompileStep { } return Ok(Self::Complete(runtime.finish_regexp_compile( realm, - regexp, + ®exp, genuine.pattern, genuine.program, )?)); } let resume = RegExpCompileResume(Box::new(RegExpCompileResumeState { realm, - regexp: regexp.clone(), - flags: flags.clone(), + regexp, + flags, phase: CompilePhase::Pattern, })); if matches!(pattern, Value::Undefined) { resume.pattern(runtime, JsString::from_static("")) } else { Ok(Self::Primitive { - value: pattern.clone(), + value: runtime.into_jsvalue(pattern)?, resume, }) } @@ -177,7 +184,7 @@ impl RegExpCompileResume { )?)) } else { Ok(RegExpCompileStep::Primitive { - value: self.0.flags.clone(), + value: runtime.into_jsvalue(self.0.flags.clone())?, resume: { let updated_0 = CompilePhase::Flags(pattern); self.0.phase = updated_0; @@ -192,7 +199,7 @@ impl RegExpCompileResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpCompileStep::Complete(Completion::Throw(value))); } @@ -205,7 +212,9 @@ impl RegExpCompileResume { let value = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpCompileStep::Complete(Completion::Throw(value))); + return Ok(RegExpCompileStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.phase { diff --git a/src/engine/builtins/regexp/constructor.rs b/src/engine/builtins/regexp/constructor.rs index 9a585334..25ca17d4 100644 --- a/src/engine/builtins/regexp/constructor.rs +++ b/src/engine/builtins/regexp/constructor.rs @@ -8,6 +8,7 @@ use crate::engine::api::error::Error; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; use crate::engine::heap::{ ContextId, ObjectData, ObjectPayload, PropertySlot, RawValue, RegExpObjectData, RegExpRealmData, @@ -15,7 +16,7 @@ use crate::engine::heap::{ use crate::engine::object::shape::{PropertyFlags, ShapeEntry}; use crate::engine::object::{ObjectRef, PropertyKey, WellKnownSymbol}; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::call::{ ConstructorPrototypeSource, ConstructorRef, NativeArguments, NativeInvocation, prototype::{ProtoSourceStep, finish as finish_source}, @@ -87,7 +88,7 @@ impl Runtime { "RegExp species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } pub(crate) fn genuine_regexp( @@ -160,7 +161,7 @@ impl Runtime { let last_index = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; let entries = [ShapeEntry { - atom: last_index.atom(), + atom: AtomIdx::from_raw(last_index.atom().raw()), flags: PropertyFlags::data(true, false, false), }]; let mut state = self.0.state.borrow_mut(); @@ -250,11 +251,11 @@ pub(crate) enum RegExpConstructorStep { resume: RegExpConstructorResume, }, Primitive { - value: Value, + value: JsValue, resume: RegExpConstructorResume, }, Prototype { - new_target: Value, + new_target: JsValue, resume: RegExpConstructorResume, }, } @@ -307,23 +308,16 @@ impl RegExpConstructorStep { "RegExp constructor did not receive constructor-or-function invocation", )); }; - let pattern = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp constructor pattern argv was not padded", - ))? - .clone(); - let flags = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "RegExp constructor flags argv was not padded", - ))? - .clone(); + let new_target = runtime.root_value(new_target)?; + let pattern = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("RegExp constructor pattern argv was not padded"), + )?)?; + let flags = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("RegExp constructor flags argv was not padded"), + )?)?; let resume = RegExpConstructorResume(Box::new(RegExpConstructorResumeState { realm, - new_target: new_target.clone(), + new_target, pattern, flags, is_regexp: false, @@ -376,11 +370,11 @@ impl RegExpConstructorResume { if matches!(self.0.flags, Value::Undefined) && let Some(genuine) = genuine.as_ref() { - return Ok(self.lookup(RegExpPublication::Copy(genuine.clone()))); + return self.lookup(runtime, RegExpPublication::Copy(genuine.clone())); } if let Some(genuine) = genuine { let flags = self.0.flags.clone(); - self.pattern_value(Value::String(genuine.pattern), flags) + self.pattern_value(runtime, Value::String(genuine.pattern), flags) } else if self.0.is_regexp { let Value::Object(object) = &self.0.pattern else { return Err(RuntimeError::Invariant( @@ -400,22 +394,26 @@ impl RegExpConstructorResume { } else { let pattern = self.0.pattern.clone(); let flags = self.0.flags.clone(); - self.pattern_value(pattern, flags) + self.pattern_value(runtime, pattern, flags) } } fn pattern_value( mut self, + runtime: &Runtime, pattern: Value, flags: Value, ) -> Result { if matches!(pattern, Value::Undefined) { - Ok(self.lookup(RegExpPublication::Compile { - pattern: JsString::from_static(""), - flags, - })) + self.lookup( + runtime, + RegExpPublication::Compile { + pattern: JsString::from_static(""), + flags, + }, + ) } else { Ok(RegExpConstructorStep::Primitive { - value: pattern, + value: runtime.into_jsvalue(pattern)?, resume: { let updated_0 = RegExpConstructorPhase::Pattern(flags); self.0.phase = updated_0; @@ -424,15 +422,19 @@ impl RegExpConstructorResume { }) } } - fn lookup(mut self, publication: RegExpPublication) -> RegExpConstructorStep { - RegExpConstructorStep::Prototype { - new_target: self.0.new_target.clone(), + fn lookup( + mut self, + runtime: &Runtime, + publication: RegExpPublication, + ) -> Result { + Ok(RegExpConstructorStep::Prototype { + new_target: runtime.into_jsvalue(self.0.new_target.clone())?, resume: { let updated_0 = RegExpConstructorPhase::Prototype(publication); self.0.phase = updated_0; self }, - } + }) } fn publish( runtime: &Runtime, @@ -443,7 +445,7 @@ impl RegExpConstructorResume { let program = Runtime::compile_regexp_program(&pattern, &flags)?; runtime.publish_regexp(&object, pattern, program)?; Ok(RegExpConstructorStep::Complete(Completion::Return( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, ))) } pub(crate) fn prototype( @@ -460,7 +462,9 @@ impl RegExpConstructorResume { )? } NativeConversion::Throw(value) => { - return Ok(RegExpConstructorStep::Complete(Completion::Throw(value))); + return Ok(RegExpConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let object = runtime.new_uninitialized_regexp(&prototype)?; @@ -473,7 +477,7 @@ impl RegExpConstructorResume { RegExpPublication::Copy(genuine) => { runtime.publish_regexp(&object, genuine.pattern, genuine.program)?; Ok(RegExpConstructorStep::Complete(Completion::Return( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, ))) } RegExpPublication::Compile { pattern, flags } => { @@ -481,7 +485,7 @@ impl RegExpConstructorResume { Self::publish(runtime, object, pattern, JsString::from_static("")) } else { Ok(RegExpConstructorStep::Primitive { - value: flags, + value: runtime.into_jsvalue(flags)?, resume: { let updated_0 = RegExpConstructorPhase::Flags { object, pattern }; self.0.phase = updated_0; @@ -498,7 +502,7 @@ impl RegExpConstructorResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpConstructorStep::Complete(Completion::Throw(value))); } @@ -516,7 +520,7 @@ impl RegExpConstructorResume { RegExpConstructorPhase::Identity(active) => { if value.same_value(&Value::Object(active)) { Ok(RegExpConstructorStep::Complete(Completion::Return( - self.0.pattern, + runtime.into_jsvalue(self.0.pattern)?, ))) } else { { @@ -546,7 +550,7 @@ impl RegExpConstructorResume { }) } else { let flags = self.0.flags.clone(); - self.pattern_value(value, flags) + self.pattern_value(runtime, value, flags) } } RegExpConstructorPhase::SourceFlags(pattern) => { @@ -554,7 +558,7 @@ impl RegExpConstructorResume { self.0.phase = updated_0; self } - .pattern_value(pattern, value), + .pattern_value(runtime, pattern, value), RegExpConstructorPhase::Pattern(flags) => { if matches!(value, Value::Object(_)) { return Err(RuntimeError::Invariant( @@ -564,7 +568,9 @@ impl RegExpConstructorResume { let pattern = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpConstructorStep::Complete(Completion::Throw(value))); + return Ok(RegExpConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok({ @@ -572,7 +578,7 @@ impl RegExpConstructorResume { self.0.phase = updated_0; self } - .lookup(RegExpPublication::Compile { pattern, flags })) + .lookup(runtime, RegExpPublication::Compile { pattern, flags })?) } RegExpConstructorPhase::Flags { object, pattern } => { if matches!(value, Value::Object(_)) { @@ -583,7 +589,9 @@ impl RegExpConstructorResume { let flags = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpConstructorStep::Complete(Completion::Throw(value))); + return Ok(RegExpConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Self::publish(runtime, object, pattern, flags) @@ -611,8 +619,8 @@ fn finish_constructor( runtime.get_property_in_realm(realm, &object, &key)?, )?, RegExpConstructorStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::String)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? } else { Completion::Return(value) }; @@ -623,7 +631,11 @@ fn finish_constructor( finish_source( runtime, realm, - ProtoSourceStep::start(runtime, realm, new_target)?, + ProtoSourceStep::start( + runtime, + realm, + runtime.root_and_release_jsvalue(new_target)?, + )?, )?, )?, }; @@ -644,33 +656,50 @@ mod tests { let new_target = context.eval("(function(){})").unwrap(); let flags = runtime.new_object(None).unwrap(); let flags_id = flags.object_id(); - let invocation = NativeInvocation::Construct { new_target }; + let invocation = NativeInvocation::Construct { + new_target: runtime.into_jsvalue(new_target).unwrap(), + }; let arguments = NativeArguments { actual_arg_count: 2, readable: vec![ - Value::String(JsString::from_static("a")), - Value::Object(flags), + runtime + .into_jsvalue(Value::String(JsString::from_static("a"))) + .unwrap(), + runtime.into_jsvalue(Value::Object(flags)).unwrap(), ], }; - let RegExpConstructorStep::Primitive { resume, .. } = + let RegExpConstructorStep::Primitive { value, resume } = RegExpConstructorStep::start(&runtime, context.realm, &invocation, &arguments).unwrap() else { panic!("expected pattern conversion") }; - drop(invocation); - drop(arguments); - let RegExpConstructorStep::Prototype { resume, .. } = resume + runtime.release_jsvalue(value).unwrap(); + { + let NativeInvocation::Construct { new_target } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(new_target).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + } + let RegExpConstructorStep::Prototype { new_target, resume } = resume .resume( &runtime, - Completion::Return(Value::String(JsString::from_static("a"))), + Completion::Return( + runtime + .into_jsvalue(Value::String(JsString::from_static("a"))) + .unwrap(), + ), ) .unwrap() else { panic!("expected prototype request") }; + runtime.release_jsvalue(new_target).unwrap(); let prototype = runtime.new_object(None).unwrap(); let prototype_id = prototype.object_id(); - let RegExpConstructorStep::Primitive { resume, .. } = resume + let RegExpConstructorStep::Primitive { value, resume } = resume .prototype( &runtime, NativeConversion::Value(ConstructorPrototypeSource::Explicit(prototype)), @@ -679,6 +708,7 @@ mod tests { else { panic!("expected flags conversion") }; + runtime.release_jsvalue(value).unwrap(); let RegExpConstructorPhase::Flags { object, .. } = &resume.phase else { panic!("expected unpublished result") }; diff --git a/src/engine/builtins/regexp/escape.rs b/src/engine/builtins/regexp/escape.rs index c7c4cc2f..30eac357 100644 --- a/src/engine/builtins/regexp/escape.rs +++ b/src/engine/builtins/regexp/escape.rs @@ -79,21 +79,22 @@ impl Runtime { "RegExp.escape did not receive a generic invocation", )); }; - let argument = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant("RegExp.escape argv was not padded"))?; - let Value::String(source) = argument else { - return Ok(Completion::Throw(self.new_native_error( + let argument = self.root_value( + arguments + .readable + .first() + .ok_or(RuntimeError::Invariant("RegExp.escape argv was not padded"))?, + )?; + let Value::String(source) = &argument else { + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not a string", )?)); }; - Ok(Completion::Return(Value::String(regexp_escape_with_limit( - source, - JsString::MAX_LEN, - )?))) + Ok(Completion::Return(self.into_jsvalue(Value::String( + regexp_escape_with_limit(source, JsString::MAX_LEN)?, + ))?)) } } diff --git a/src/engine/builtins/regexp/exec.rs b/src/engine/builtins/regexp/exec.rs index 5cb97cd3..6678ae26 100644 --- a/src/engine/builtins/regexp/exec.rs +++ b/src/engine/builtins/regexp/exec.rs @@ -8,7 +8,7 @@ use crate::engine::heap::ContextId; use crate::engine::object::{CompleteOrdinaryPropertyDescriptor, ObjectRef, PropertyKey}; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::{Completion, ToPrimitiveHint}; use crate::engine::vm::call::{DirectCallTarget, NativeArguments, NativeInvocation}; @@ -82,14 +82,14 @@ impl Runtime { match execution { Ok(value) => value, Err(ExecError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory in regexp execution", )?)); } Err(ExecError::Interrupted) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "interrupted", @@ -112,9 +112,9 @@ impl Runtime { if updates_last_index && let Some(exception) = self.set_regexp_last_index(realm, object, 0)? { - return Ok(Completion::Throw(exception)); + return Ok(Completion::Throw(self.into_jsvalue(exception)?)); } - return Ok(Completion::Return(Value::Null)); + return Ok(Completion::Return(JsValue::Null)); }; let complete = matched.capture(0).ok_or(RuntimeError::Invariant( @@ -126,12 +126,13 @@ impl Runtime { })?; // This write happens before any result/indices allocation. if let Some(exception) = self.set_regexp_last_index(realm, object, end)? { - return Ok(Completion::Throw(exception)); + return Ok(Completion::Throw(self.into_jsvalue(exception)?)); } } - self.build_regexp_result(realm, input, program, matched) - .map(Completion::Return) + Ok(Completion::Return(self.into_jsvalue( + self.build_regexp_result(realm, input, program, matched)?, + )?)) } fn regexp_last_index_value(&self, object: &ObjectRef) -> Result { @@ -207,26 +208,21 @@ impl RegExpExecStep { "RegExp exec/test did not receive a generic invocation", )); }; - let input = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp exec/test input argv was not padded", - ))? - .clone(); + let input = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("RegExp exec/test input argv was not padded"), + )?)?; + let this_value = runtime.root_value(this_value)?; match kind { RegExpNativeKind::Exec => RegExpExecResume(Box::new(RegExpExecResumeState { step_pending: RegExpExecStepPending::default(), realm, - regexp: this_value.clone(), + regexp: this_value, input, test: false, phase: ExecPhase::Input, })) .builtin(runtime), - RegExpNativeKind::Test => { - Self::abstract_start(runtime, realm, this_value.clone(), input, true) - } + RegExpNativeKind::Test => Self::abstract_start(runtime, realm, this_value, input, true), _ => Err(RuntimeError::Invariant( "non-exec RegExp selector reached exec dispatch", )), @@ -255,7 +251,7 @@ impl RegExpExecStep { "undefined" }; return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!("cannot read property 'exec' of {base}"), @@ -263,7 +259,7 @@ impl RegExpExecStep { ))); } Ok(Self::make_read( - regexp.clone(), + runtime.unroot_value(®exp)?, key, RegExpExecResume(Box::new(RegExpExecResumeState { step_pending: RegExpExecStepPending::default(), @@ -280,7 +276,7 @@ impl RegExpExecResume { fn complete(&mut self, result: Completion) -> RegExpExecStep { RegExpExecStep::Complete(match result { Completion::Return(value) if self.0.test => { - Completion::Return(Value::Bool(!matches!(value, Value::Null))) + Completion::Return(JsValue::Bool(!matches!(value, JsValue::Null))) } result => result, }) @@ -289,11 +285,13 @@ impl RegExpExecResume { if !matches!(&self.0.regexp, Value::Object(_)) || runtime.genuine_regexp(&self.0.regexp)?.is_none() { - return Ok(self.complete(Completion::Throw(runtime.new_native_error( - self.0.realm, - NativeErrorKind::Type, - "RegExp object expected", - )?))); + return Ok( + self.complete(Completion::Throw(runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "RegExp object expected", + )?)), + ); } let input = self.0.input.clone(); let resume = { @@ -303,12 +301,12 @@ impl RegExpExecResume { }; if matches!(input, Value::Object(_)) { Ok(RegExpExecStep::make_primitive( - input, + runtime.into_jsvalue(input)?, ToPrimitiveHint::String, resume, )) } else { - resume.resume(runtime, Completion::Return(input)) + resume.resume(runtime, Completion::Return(runtime.into_jsvalue(input)?)) } } pub(crate) fn resume( @@ -317,7 +315,7 @@ impl RegExpExecResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(self.complete(Completion::Throw(value))), }; match self.0.phase { @@ -331,16 +329,18 @@ impl RegExpExecResume { }; let mut arguments = Vec::new(); if arguments.try_reserve_exact(1).is_err() { - return Ok(self.complete(Completion::Throw(runtime.new_native_error( - self.0.realm, - NativeErrorKind::Internal, - "out of memory", - )?))); + return Ok(self.complete(Completion::Throw( + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Internal, + "out of memory", + )?, + ))); } - arguments.push(self.0.input.clone()); + arguments.push(runtime.unroot_value(&self.0.input)?); Ok(RegExpExecStep::make_call( DirectCallTarget::Callable(callable), - self.0.regexp.clone(), + runtime.unroot_value(&self.0.regexp)?, arguments, { let updated_0 = ExecPhase::Called; @@ -351,13 +351,15 @@ impl RegExpExecResume { } ExecPhase::Called => { if matches!(value, Value::Object(_) | Value::Null) { - Ok(self.complete(Completion::Return(value))) + Ok(self.complete(Completion::Return(runtime.into_jsvalue(value)?))) } else { - Ok(self.complete(Completion::Throw(runtime.new_native_error( - self.0.realm, - NativeErrorKind::Type, - "RegExp exec method must return an object or null", - )?))) + Ok( + self.complete(Completion::Throw(runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "RegExp exec method must return an object or null", + )?)), + ) } } ExecPhase::Input => { @@ -369,7 +371,7 @@ impl RegExpExecResume { let input = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(input) => input, NativeConversion::Throw(value) => { - return Ok(self.complete(Completion::Throw(value))); + return Ok(self.complete(Completion::Throw(runtime.into_jsvalue(value)?))); } }; let Value::Object(object) = &self.0.regexp else { @@ -385,14 +387,14 @@ impl RegExpExecResume { }; if matches!(value, Value::Object(_)) { Ok(RegExpExecStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::Number, resume, )) } else { // No callback: the branded receiver and input stay owned by // this domain; the generic conversion/Query is never built. - resume.resume(runtime, Completion::Return(value)) + resume.resume(runtime, Completion::Return(runtime.into_jsvalue(value)?)) } } ExecPhase::LastIndex(input) => { @@ -404,7 +406,9 @@ impl RegExpExecResume { let last_index = match runtime.native_to_length(self.0.realm, &value)? { NativeConversion::Value(index) => index, NativeConversion::Throw(value) => { - return Ok(RegExpExecStep::Complete(Completion::Throw(value))); + return Ok(RegExpExecStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Value::Object(object) = &self.0.regexp else { @@ -433,7 +437,7 @@ fn finish( step = match step { RegExpExecStep::Complete(result) => return Ok(result), RegExpExecStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -444,8 +448,8 @@ fn finish( let value = resume.take_primitive_value(); let hint = resume.take_primitive_hint(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; @@ -454,8 +458,12 @@ fn finish( } RegExpExecStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -480,12 +488,17 @@ mod local_exec_tests { fn primitive_regexp_exec_completes_inside_its_domain() { let runtime = Runtime::new(); let mut context = runtime.new_context(); - let invocation = NativeInvocation::Call { - this_value: context.eval("/a/g").unwrap(), - }; + let this_value = runtime + .unroot_value(&context.eval("/a/g").unwrap()) + .unwrap(); + let invocation = NativeInvocation::Call { this_value }; let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::String(JsString::from_static("a"))], + readable: vec![ + runtime + .unroot_value(&Value::String(JsString::from_static("a"))) + .unwrap(), + ], }; assert!(matches!( RegExpExecStep::start( @@ -496,7 +509,7 @@ mod local_exec_tests { &arguments ) .unwrap(), - RegExpExecStep::Complete(Completion::Return(Value::Object(_))) + RegExpExecStep::Complete(Completion::Return(JsValue::Object(_))) )); } @@ -519,16 +532,16 @@ mod local_exec_tests { #[derive(Default)] pub(crate) struct RegExpExecStepPending { - receiver: Option, + receiver: Option, key: Option, - value: Option, + value: Option, hint: Option, target: Option, - arguments: Option>, + arguments: Option>, } impl RegExpExecStep { pub(crate) fn make_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: RegExpExecResume, ) -> Self { @@ -537,7 +550,7 @@ impl RegExpExecStep { Self::Read { resume } } pub(crate) fn make_primitive( - value: Value, + value: JsValue, hint: ToPrimitiveHint, mut resume: RegExpExecResume, ) -> Self { @@ -547,8 +560,8 @@ impl RegExpExecStep { } pub(crate) fn make_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: RegExpExecResume, ) -> Self { resume.0.step_pending.target = Some(target); @@ -558,7 +571,7 @@ impl RegExpExecStep { } } impl RegExpExecResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .step_pending .receiver @@ -573,7 +586,7 @@ impl RegExpExecResume { .expect("RegExpExecStep::Read lost key") } - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -595,14 +608,14 @@ impl RegExpExecResume { .take() .expect("RegExpExecStep::Call lost target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .step_pending .receiver .take() .expect("RegExpExecStep::Call lost receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .step_pending .arguments diff --git a/src/engine/builtins/regexp/iterator_next.rs b/src/engine/builtins/regexp/iterator_next.rs index 7be19056..e9f67f91 100644 --- a/src/engine/builtins/regexp/iterator_next.rs +++ b/src/engine/builtins/regexp/iterator_next.rs @@ -4,7 +4,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, ObjectPayload}, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeInvocation, NativeInvokeOutcome}, @@ -66,31 +66,34 @@ impl RegExpIteratorStep { "RegExp String Iterator next did not receive an iterator-next invocation", )); }; - let iterator = match this_value { - Value::Object(iterator) - if matches!( - runtime - .0 - .state - .borrow() - .heap - .object(iterator.object_id())? - .payload, - ObjectPayload::RegExpStringIterator { .. } - ) => - { - iterator.clone() - } - _ => { - return Ok(Self::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(runtime.new_native_error( - realm, - NativeErrorKind::Type, - "RegExp String Iterator object expected", - )?), - ))); - } + let JsValue::Object(iterator_id) = this_value else { + return Ok(Self::Complete(NativeInvokeOutcome::Completion( + Completion::Throw(runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "RegExp String Iterator object expected", + )?), + ))); }; + let iterator = ObjectRef::from_borrowed_handle(runtime.clone(), *iterator_id)?; + if !matches!( + runtime + .0 + .state + .borrow() + .heap + .object(iterator.object_id())? + .payload, + ObjectPayload::RegExpStringIterator { .. } + ) { + return Ok(Self::Complete(NativeInvokeOutcome::Completion( + Completion::Throw(runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "RegExp String Iterator object expected", + )?), + ))); + } let (regexp_id, string, global, full_unicode, done) = runtime .0 .state @@ -99,14 +102,14 @@ impl RegExpIteratorStep { .regexp_string_iterator_state(iterator.object_id())?; if done { return Ok(Self::Complete(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, })); } let regexp = ObjectRef::from_borrowed_handle(runtime.clone(), regexp_id)?; Ok(Self::make_exec( - Value::Object(regexp.clone()), - Value::String(string.clone()), + runtime.into_jsvalue(Value::Object(regexp.clone()))?, + runtime.into_jsvalue(Value::String(string.clone()))?, RegExpIteratorResume(Box::new(RegExpIteratorResumeState { step_pending: RegExpIteratorStepPending::default(), scheduler_set_key: None, @@ -133,16 +136,19 @@ impl RegExpIteratorResume { self.0.scheduler_set_key.take().expect("waiting Set key") } - fn abrupt(self, value: Value) -> RegExpIteratorStep { - RegExpIteratorStep::Complete(NativeInvokeOutcome::Completion(Completion::Throw(value))) + fn abrupt(self, runtime: &Runtime, value: Value) -> Result { + Ok(RegExpIteratorStep::Complete( + NativeInvokeOutcome::Completion(Completion::Throw(runtime.into_jsvalue(value)?)), + )) } fn yielded(self) -> Result { Ok(RegExpIteratorStep::Complete( NativeInvokeOutcome::IteratorNextRaw { - value: Value::Object( + value: JsValue::Object( self.0 .matched - .ok_or(RuntimeError::Invariant("RegExp iterator lost match result"))?, + .ok_or(RuntimeError::Invariant("RegExp iterator lost match result"))? + .into_handle(), ), done: false, }, @@ -154,8 +160,10 @@ impl RegExpIteratorResume { reply: Completion, ) -> Result { let value = match reply { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(self.abrupt(value)), + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return self.abrupt(runtime, runtime.root_and_release_jsvalue(value)?); + } }; match self.0.phase { Phase::Exec => { @@ -169,7 +177,7 @@ impl RegExpIteratorResume { .finish_regexp_string_iterator(self.0.iterator.object_id())?; return Ok(RegExpIteratorStep::Complete( NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }, )); @@ -200,12 +208,18 @@ impl RegExpIteratorResume { } Phase::MatchString => { self.0.match_value = value.clone(); - Ok(RegExpIteratorStep::make_string(value, self)) + Ok(RegExpIteratorStep::make_string( + runtime.into_jsvalue(value)?, + self, + )) } Phase::LastIndex => { self.0.index_value = value.clone(); self.0.phase = Phase::Advance; - Ok(RegExpIteratorStep::make_primitive(value, self)) + Ok(RegExpIteratorStep::make_primitive( + runtime.into_jsvalue(value)?, + self, + )) } Phase::Advance => { if matches!(value, Value::Object(_)) { @@ -215,7 +229,9 @@ impl RegExpIteratorResume { } let current = match runtime.native_to_length(self.0.realm, &value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return self.abrupt(runtime, value); + } }; let next = advance_string_index(&self.0.string, current, self.0.full_unicode); self.0.phase = Phase::Set; @@ -223,7 +239,7 @@ impl RegExpIteratorResume { self.0.regexp.clone(), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?, - Value::number(next as f64), + runtime.into_jsvalue(Value::number(next as f64))?, self, )) } @@ -244,7 +260,7 @@ impl RegExpIteratorResume { } let string = match reply { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => return self.abrupt(runtime, value), }; if !string.is_empty() { return self.yielded(); @@ -268,7 +284,7 @@ impl RegExpIteratorResume { )); } if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, reply)? { - return Ok(self.abrupt(value)); + return self.abrupt(runtime, value); } self.yielded() } @@ -282,8 +298,8 @@ pub(crate) fn finish( step = match step { RegExpIteratorStep::Complete(result) => return Ok(result), RegExpIteratorStep::Exec { mut resume } => { - let regexp = resume.take_exec_regexp(); - let input = resume.take_exec_input(); + let regexp = runtime.root_and_release_jsvalue(resume.take_exec_regexp())?; + let input = runtime.root_and_release_jsvalue(resume.take_exec_input())?; resume.resume(runtime, runtime.regexp_exec_abstract(realm, regexp, input)?)? } RegExpIteratorStep::Read { mut resume } => { @@ -296,19 +312,23 @@ pub(crate) fn finish( } RegExpIteratorStep::String { mut resume } => { let value = resume.take_string_value(); - resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? + resume.string( + runtime, + runtime + .native_to_js_string(realm, &runtime.root_and_release_jsvalue(value)?)?, + )? } RegExpIteratorStep::Primitive { mut resume } => { let value = resume.take_primitive_value(); resume.resume( runtime, - runtime.to_primitive(realm, value, ToPrimitiveHint::Number)?, + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::Number)?, )? } RegExpIteratorStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; resume.set( runtime, key.clone(), @@ -327,14 +347,18 @@ pub(crate) fn finish( #[derive(Default)] pub(crate) struct RegExpIteratorStepPending { - regexp: Option, - input: Option, + regexp: Option, + input: Option, object: Option, key: Option, - value: Option, + value: Option, } impl RegExpIteratorStep { - pub(crate) fn make_exec(regexp: Value, input: Value, mut resume: RegExpIteratorResume) -> Self { + pub(crate) fn make_exec( + regexp: JsValue, + input: JsValue, + mut resume: RegExpIteratorResume, + ) -> Self { resume.0.step_pending.regexp = Some(regexp); resume.0.step_pending.input = Some(input); Self::Exec { resume } @@ -348,18 +372,18 @@ impl RegExpIteratorStep { resume.0.step_pending.key = Some(key); Self::Read { resume } } - pub(crate) fn make_string(value: Value, mut resume: RegExpIteratorResume) -> Self { + pub(crate) fn make_string(value: JsValue, mut resume: RegExpIteratorResume) -> Self { resume.0.step_pending.value = Some(value); Self::String { resume } } - pub(crate) fn make_primitive(value: Value, mut resume: RegExpIteratorResume) -> Self { + pub(crate) fn make_primitive(value: JsValue, mut resume: RegExpIteratorResume) -> Self { resume.0.step_pending.value = Some(value); Self::Primitive { resume } } pub(crate) fn make_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: RegExpIteratorResume, ) -> Self { resume.0.step_pending.object = Some(object); @@ -369,14 +393,14 @@ impl RegExpIteratorStep { } } impl RegExpIteratorResume { - pub(crate) fn take_exec_regexp(&mut self) -> Value { + pub(crate) fn take_exec_regexp(&mut self) -> JsValue { self.0 .step_pending .regexp .take() .expect("RegExpIteratorStep::Exec lost regexp") } - pub(crate) fn take_exec_input(&mut self) -> Value { + pub(crate) fn take_exec_input(&mut self) -> JsValue { self.0 .step_pending .input @@ -399,7 +423,7 @@ impl RegExpIteratorResume { .expect("RegExpIteratorStep::Read lost key") } - pub(crate) fn take_string_value(&mut self) -> Value { + pub(crate) fn take_string_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -407,7 +431,7 @@ impl RegExpIteratorResume { .expect("RegExpIteratorStep::String lost value") } - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -429,7 +453,7 @@ impl RegExpIteratorResume { .take() .expect("RegExpIteratorStep::Set lost key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .step_pending .value diff --git a/src/engine/builtins/regexp/match_all.rs b/src/engine/builtins/regexp/match_all.rs index ce743317..124d87e6 100644 --- a/src/engine/builtins/regexp/match_all.rs +++ b/src/engine/builtins/regexp/match_all.rs @@ -81,9 +81,12 @@ impl Runtime { ) -> Result { match self.call_regexp_string_iterator_next_raw(realm, invocation)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_iterator_result(realm, value, done)?, + ))?)) + } } } diff --git a/src/engine/builtins/regexp/match_all_protocol.rs b/src/engine/builtins/regexp/match_all_protocol.rs index fc5bfcc7..fffa42fa 100644 --- a/src/engine/builtins/regexp/match_all_protocol.rs +++ b/src/engine/builtins/regexp/match_all_protocol.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{ConstructorRef, NativeArguments, NativeInvocation}, @@ -79,24 +79,21 @@ impl RegExpMatchAllStep { "RegExp @@matchAll did not receive a generic invocation", )); }; + let this_value = runtime.root_value(this_value)?; let Value::Object(regexp) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; Ok(Self::make_primitive( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp @@matchAll input argv was not padded", - ))? - .clone(), + runtime.dup_jsvalue(arguments.readable.first().ok_or(RuntimeError::Invariant( + "RegExp @@matchAll input argv was not padded", + ))?)?, ToPrimitiveHint::String, RegExpMatchAllResume(Box::new(RegExpMatchAllResumeState { step_pending: RegExpMatchAllStepPending::default(), realm, - regexp: regexp.clone(), + regexp, phase: Phase::Input, })), )) @@ -111,7 +108,9 @@ impl RegExpMatchAllResume { let constructor = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchAllStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchAllStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Species(input) = self.0.phase else { @@ -137,8 +136,8 @@ impl RegExpMatchAllResume { let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; let completion = match runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - Some(value) => Completion::Throw(value), - None => Completion::Return(Value::Undefined), + Some(value) => Completion::Throw(runtime.into_jsvalue(value)?), + None => Completion::Return(JsValue::Undefined), }; self.resume(runtime, completion) } @@ -148,7 +147,7 @@ impl RegExpMatchAllResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpMatchAllStep::Complete(Completion::Throw(value))); } @@ -158,7 +157,9 @@ impl RegExpMatchAllResume { let input = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchAllStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchAllStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(RegExpMatchAllStep::make_species(self.0.regexp.clone(), { @@ -168,7 +169,7 @@ impl RegExpMatchAllResume { })) } Phase::Flags { input, constructor } => Ok(RegExpMatchAllStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::String, { let updated_0 = Phase::FlagsPrimitive { input, constructor }; @@ -180,21 +181,23 @@ impl RegExpMatchAllResume { let flags = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchAllStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchAllStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let mut arguments = Vec::new(); if arguments.try_reserve_exact(2).is_err() { return Ok(RegExpMatchAllStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Internal, "out of memory", )?, ))); } - arguments.push(Value::Object(self.0.regexp.clone())); - arguments.push(Value::String(flags.clone())); + arguments.push(runtime.into_jsvalue(Value::Object(self.0.regexp.clone()))?); + arguments.push(runtime.into_jsvalue(Value::String(flags.clone()))?); Ok(RegExpMatchAllStep::make_construct( constructor, arguments, @@ -231,7 +234,7 @@ impl RegExpMatchAllResume { flags, matcher, } => Ok(RegExpMatchAllStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::Number, { let updated_0 = Phase::LastIndexPrimitive { @@ -251,14 +254,16 @@ impl RegExpMatchAllResume { let length = match runtime.native_to_length(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchAllStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchAllStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(RegExpMatchAllStep::make_set( matcher.clone(), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?, - Value::number(length as f64), + runtime.into_jsvalue(Value::number(length as f64))?, { let updated_0 = Phase::Set { input, @@ -280,13 +285,13 @@ impl RegExpMatchAllResume { .utf16_units() .any(|unit| unit == u16::from(b'u') || unit == u16::from(b'v')); Ok(RegExpMatchAllStep::Complete(Completion::Return( - Value::Object(runtime.new_regexp_string_iterator( + runtime.into_jsvalue(Value::Object(runtime.new_regexp_string_iterator( self.0.realm, &matcher, input, global, full_unicode, - )?), + )?))?, ))) } Phase::Species(_) => Err(RuntimeError::Invariant( @@ -307,8 +312,8 @@ pub(super) fn finish( let value = resume.take_primitive_value(); let hint = resume.take_primitive_hint(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; @@ -329,7 +334,11 @@ pub(super) fn finish( } RegExpMatchAllStep::Construct { mut resume } => { let constructor = resume.take_construct_constructor(); - let arguments = resume.take_construct_arguments(); + let arguments = resume + .take_construct_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.construct_constructor_internal( @@ -343,7 +352,7 @@ pub(super) fn finish( RegExpMatchAllStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; resume.set( runtime, runtime.internal_set( @@ -361,17 +370,17 @@ pub(super) fn finish( #[derive(Default)] pub(crate) struct RegExpMatchAllStepPending { - value: Option, + value: Option, hint: Option, object: Option, key: Option, regexp: Option, constructor: Option, - arguments: Option>, + arguments: Option>, } impl RegExpMatchAllStep { pub(crate) fn make_primitive( - value: Value, + value: JsValue, hint: ToPrimitiveHint, mut resume: RegExpMatchAllResume, ) -> Self { @@ -394,7 +403,7 @@ impl RegExpMatchAllStep { } pub(crate) fn make_construct( constructor: ConstructorRef, - arguments: Vec, + arguments: Vec, mut resume: RegExpMatchAllResume, ) -> Self { resume.0.step_pending.constructor = Some(constructor); @@ -404,7 +413,7 @@ impl RegExpMatchAllStep { pub(crate) fn make_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: RegExpMatchAllResume, ) -> Self { resume.0.step_pending.object = Some(object); @@ -414,7 +423,7 @@ impl RegExpMatchAllStep { } } impl RegExpMatchAllResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -459,7 +468,7 @@ impl RegExpMatchAllResume { .take() .expect("RegExpMatchAllStep::Construct lost constructor") } - pub(crate) fn take_construct_arguments(&mut self) -> Vec { + pub(crate) fn take_construct_arguments(&mut self) -> Vec { self.0 .step_pending .arguments @@ -481,7 +490,7 @@ impl RegExpMatchAllResume { .take() .expect("RegExpMatchAllStep::Set lost key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .step_pending .value diff --git a/src/engine/builtins/regexp/match_protocol.rs b/src/engine/builtins/regexp/match_protocol.rs index 56c54c6d..669318f8 100644 --- a/src/engine/builtins/regexp/match_protocol.rs +++ b/src/engine/builtins/regexp/match_protocol.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -83,40 +83,41 @@ impl RegExpMatchStep { "RegExp @@match did not receive a generic invocation", )); }; + let this_value = runtime.root_value(this_value)?; let Value::Object(regexp) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; Ok(Self::make_primitive( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp @@match input argv was not padded", - ))? - .clone(), + runtime.dup_jsvalue(arguments.readable.first().ok_or(RuntimeError::Invariant( + "RegExp @@match input argv was not padded", + ))?)?, ToPrimitiveHint::String, RegExpMatchResume(Box::new(RegExpMatchResumeState { step_pending: RegExpMatchStepPending::default(), realm, - regexp: regexp.clone(), + regexp, phase: MatchPhase::Input, })), )) } } impl RegExpMatchResume { - fn execute(mut self, state: MatchCollection) -> RegExpMatchStep { - RegExpMatchStep::make_exec( - Value::Object(self.0.regexp.clone()), - Value::String(state.input.clone()), + fn execute( + mut self, + runtime: &Runtime, + state: MatchCollection, + ) -> Result { + Ok(RegExpMatchStep::make_exec( + runtime.into_jsvalue(Value::Object(self.0.regexp.clone()))?, + runtime.into_jsvalue(Value::String(state.input.clone()))?, { let updated_0 = MatchPhase::Exec(state); self.0.phase = updated_0; self }, - ) + )) } pub(crate) fn set( mut self, @@ -126,32 +127,37 @@ impl RegExpMatchResume { let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(RegExpMatchStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } match self.0.phase { MatchPhase::InitialSet { input, unicode } => { let matches = runtime.new_array(self.0.realm)?; let zero = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Literal1)?; - Ok({ + { let updated_0 = MatchPhase::Single; self.0.phase = updated_0; self } - .execute(MatchCollection { - input, - unicode, - matches, - zero, - count: 0, - })) + .execute( + runtime, + MatchCollection { + input, + unicode, + matches, + zero, + count: 0, + }, + ) } - MatchPhase::AdvancedSet(state) => Ok({ + MatchPhase::AdvancedSet(state) => { let updated_0 = MatchPhase::Single; self.0.phase = updated_0; self } - .execute(state)), + .execute(runtime, state), _ => Err(RuntimeError::Invariant( "RegExp match received an unexpected Set reply", )), @@ -163,7 +169,7 @@ impl RegExpMatchResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpMatchStep::Complete(Completion::Throw(value))); } @@ -173,7 +179,9 @@ impl RegExpMatchResume { let input = match match_string(runtime, self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(RegExpMatchStep::make_read( @@ -187,7 +195,7 @@ impl RegExpMatchResume { )) } MatchPhase::Flags(input) => Ok(RegExpMatchStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::String, { let updated_0 = MatchPhase::FlagsString(input); @@ -199,13 +207,15 @@ impl RegExpMatchResume { let flags = match match_string(runtime, self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !flags.utf16_units().any(|unit| unit == u16::from(b'g')) { return Ok(RegExpMatchStep::make_exec( - Value::Object(self.0.regexp.clone()), - Value::String(input), + runtime.into_jsvalue(Value::Object(self.0.regexp.clone()))?, + runtime.into_jsvalue(Value::String(input))?, { let updated_0 = MatchPhase::Single; self.0.phase = updated_0; @@ -220,7 +230,7 @@ impl RegExpMatchResume { self.0.regexp.clone(), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?, - Value::Int(0), + runtime.into_jsvalue(Value::Int(0))?, { let updated_0 = MatchPhase::InitialSet { input, unicode }; self.0.phase = updated_0; @@ -228,16 +238,18 @@ impl RegExpMatchResume { }, )) } - MatchPhase::Single => Ok(RegExpMatchStep::Complete(Completion::Return(value))), + MatchPhase::Single => Ok(RegExpMatchStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), MatchPhase::Exec(state) => { let result = match value { Value::Null => { return Ok(RegExpMatchStep::Complete(Completion::Return( - if state.count == 0 { + runtime.into_jsvalue(if state.count == 0 { Value::Null } else { Value::Object(state.matches) - }, + })?, ))); } Value::Object(result) => result, @@ -254,7 +266,7 @@ impl RegExpMatchResume { })) } MatchPhase::Match(state) => Ok(RegExpMatchStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::String, { let updated_0 = MatchPhase::MatchString(state); @@ -266,13 +278,15 @@ impl RegExpMatchResume { let matched = match match_string(runtime, self.0.realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let empty = matched.is_empty(); let Some(next) = state.count.checked_add(1) else { return Ok(RegExpMatchStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid array length", @@ -302,16 +316,16 @@ impl RegExpMatchResume { }, )) } else { - Ok({ + { let updated_0 = MatchPhase::Single; self.0.phase = updated_0; self } - .execute(state)) + .execute(runtime, state) } } MatchPhase::LastIndex(state) => Ok(RegExpMatchStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::Number, { let updated_0 = MatchPhase::LastIndexNumber(state); @@ -328,7 +342,9 @@ impl RegExpMatchResume { let current = match runtime.native_to_length(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpMatchStep::Complete(Completion::Throw(value))); + return Ok(RegExpMatchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let next = advance_string_index(&state.input, current, state.unicode); @@ -336,7 +352,7 @@ impl RegExpMatchResume { self.0.regexp.clone(), runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?, - Value::number(next as f64), + runtime.into_jsvalue(Value::number(next as f64))?, { let updated_0 = MatchPhase::AdvancedSet(state); self.0.phase = updated_0; @@ -377,8 +393,8 @@ impl Runtime { let value = resume.take_primitive_value(); let hint = resume.take_primitive_hint(); { - let result = if matches!(value, Value::Object(_)) { - self.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + self.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; @@ -391,14 +407,14 @@ impl Runtime { resume.resume(self, self.get_property_in_realm(realm, &object, &key)?)? } RegExpMatchStep::Exec { mut resume } => { - let regexp = resume.take_exec_regexp(); - let input = resume.take_exec_input(); + let regexp = self.root_and_release_jsvalue(resume.take_exec_regexp())?; + let input = self.root_and_release_jsvalue(resume.take_exec_input())?; resume.resume(self, self.regexp_exec_abstract(realm, regexp, input)?)? } RegExpMatchStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = self.root_and_release_jsvalue(resume.take_set_value())?; resume.set( self, self.internal_set( @@ -417,16 +433,16 @@ impl Runtime { #[derive(Default)] pub(crate) struct RegExpMatchStepPending { - value: Option, + value: Option, hint: Option, object: Option, key: Option, - regexp: Option, - input: Option, + regexp: Option, + input: Option, } impl RegExpMatchStep { pub(crate) fn make_primitive( - value: Value, + value: JsValue, hint: ToPrimitiveHint, mut resume: RegExpMatchResume, ) -> Self { @@ -446,7 +462,7 @@ impl RegExpMatchStep { pub(crate) fn make_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: RegExpMatchResume, ) -> Self { resume.0.step_pending.object = Some(object); @@ -454,14 +470,18 @@ impl RegExpMatchStep { resume.0.step_pending.value = Some(value); Self::Set { resume } } - pub(crate) fn make_exec(regexp: Value, input: Value, mut resume: RegExpMatchResume) -> Self { + pub(crate) fn make_exec( + regexp: JsValue, + input: JsValue, + mut resume: RegExpMatchResume, + ) -> Self { resume.0.step_pending.regexp = Some(regexp); resume.0.step_pending.input = Some(input); Self::Exec { resume } } } impl RegExpMatchResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -505,7 +525,7 @@ impl RegExpMatchResume { .take() .expect("RegExpMatchStep::Set lost key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -513,14 +533,14 @@ impl RegExpMatchResume { .expect("RegExpMatchStep::Set lost value") } - pub(crate) fn take_exec_regexp(&mut self) -> Value { + pub(crate) fn take_exec_regexp(&mut self) -> JsValue { self.0 .step_pending .regexp .take() .expect("RegExpMatchStep::Exec lost regexp") } - pub(crate) fn take_exec_input(&mut self) -> Value { + pub(crate) fn take_exec_input(&mut self) -> JsValue { self.0 .step_pending .input diff --git a/src/engine/builtins/regexp/mod.rs b/src/engine/builtins/regexp/mod.rs index 9c2a8ea1..f574c040 100644 --- a/src/engine/builtins/regexp/mod.rs +++ b/src/engine/builtins/regexp/mod.rs @@ -51,6 +51,7 @@ use crate::engine::heap::RegExpRealmData; use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, + atom::AtomIdx, builtins::native::NativeFunctionId, heap::ContextId, object::{ @@ -274,7 +275,7 @@ impl Runtime { let last_index = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; let entries = [ShapeEntry { - atom: last_index.atom(), + atom: AtomIdx::from_raw(last_index.atom().raw()), flags: PropertyFlags::data(true, false, false), }]; let object_shape = self @@ -296,7 +297,7 @@ impl Runtime { .iter() .enumerate() .map(|(index, key)| ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: if index == 0 { PropertyFlags::data(true, false, false) } else { diff --git a/src/engine/builtins/regexp/prototype.rs b/src/engine/builtins/regexp/prototype.rs index 86cbd404..5c8409f4 100644 --- a/src/engine/builtins/regexp/prototype.rs +++ b/src/engine/builtins/regexp/prototype.rs @@ -8,7 +8,7 @@ use crate::engine::builtins::native::{RegExpFlagKind, RegExpNativeKind}; use crate::engine::heap::{ContextId, ObjectPayload, RegExpObjectData}; use crate::engine::object::{ObjectRef, PropertyKey}; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, JsStringBuilder, JsStringError, Value}; +use crate::engine::value::{JsString, JsStringBuilder, JsStringError, JsValue, Value}; use crate::engine::vm::call::NativeInvocation; use crate::engine::vm::{Completion, ToPrimitiveHint}; use crate::regexp::RegExpFlags; @@ -25,6 +25,7 @@ impl Runtime { "RegExp accessor did not receive a getter invocation", )); }; + let this_value = self.root_value(&this_value)?; match kind { RegExpNativeKind::Source => self.call_regexp_source(realm, &this_value), RegExpNativeKind::Flags => self.call_regexp_flags(realm, &this_value), @@ -64,16 +65,16 @@ impl Runtime { this_value: &Value, ) -> Result { let Value::Object(object) = this_value else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an object", )?)); }; if object.object_id() == self.regexp_realm_data(realm)?.prototype { - return Ok(Completion::Return(Value::String(JsString::from_static( - "(?:)", - )))); + return Ok(Completion::Return( + self.into_jsvalue(Value::String(JsString::from_static("(?:)")))?, + )); } let pattern = { let state = self.0.state.borrow(); @@ -125,20 +126,20 @@ impl Runtime { } }; let Some(pattern) = pattern else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "RegExp object expected", )?)); }; if pattern.is_empty() { - return Ok(Completion::Return(Value::String(JsString::from_static( - "(?:)", - )))); + return Ok(Completion::Return( + self.into_jsvalue(Value::String(JsString::from_static("(?:)")))?, + )); } - Ok(Completion::Return(Value::String(escape_regexp_source( - &pattern, - )?))) + Ok(Completion::Return(self.into_jsvalue(Value::String( + escape_regexp_source(&pattern)?, + ))?)) } fn call_regexp_flag( @@ -148,7 +149,7 @@ impl Runtime { flag: RegExpFlagKind, ) -> Result { let Value::Object(object) = this_value else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an object", @@ -204,14 +205,14 @@ impl Runtime { } }; if let Some(flags) = flags { - return Ok(Completion::Return(Value::Bool( + return Ok(Completion::Return(JsValue::Bool( flags.contains(regexp_flag_mask(flag)), ))); } if object.object_id() == self.regexp_realm_data(realm)?.prototype { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); } - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "RegExp object expected", @@ -223,18 +224,16 @@ impl Runtime { realm: ContextId, this_value: &Value, ) -> Result { - finish_presentation( - self, - realm, - RegExpPresentationStep::start( + let invocation = NativeInvocation::Getter { + this_value: self.unroot_value(this_value)?, + }; + self.dispatch_borrowed_invocation(invocation, |invocation| { + finish_presentation( self, realm, - RegExpNativeKind::Flags, - &NativeInvocation::Getter { - this_value: this_value.clone(), - }, - )?, - ) + RegExpPresentationStep::start(self, realm, RegExpNativeKind::Flags, invocation)?, + ) + }) } } @@ -325,7 +324,7 @@ pub(crate) enum RegExpPresentationStep { resume: RegExpPresentationResume, }, Primitive { - value: Value, + value: JsValue, resume: RegExpPresentationResume, }, } @@ -373,19 +372,22 @@ impl RegExpPresentationStep { )); } }; + let this_value = runtime.root_value(this_value)?; if matches!(kind, RegExpNativeKind::Source) { return Ok(Self::Complete( - runtime.call_regexp_source(realm, this_value)?, + runtime.call_regexp_source(realm, &this_value)?, )); } if let RegExpNativeKind::Flag(flag) = kind { - return Ok(Self::Complete( - runtime.call_regexp_flag(realm, this_value, flag)?, - )); + return Ok(Self::Complete(runtime.call_regexp_flag( + realm, + &this_value, + flag, + )?)); } let Value::Object(object) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; let (phase, name) = if matches!(kind, RegExpNativeKind::ToString) { @@ -417,14 +419,14 @@ impl RegExpPresentationResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpPresentationStep::Complete(Completion::Throw(value))); } }; match self.0.phase { PresentationPhase::Source => Ok(RegExpPresentationStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: { let updated_0 = PresentationPhase::SourceString; self.0.phase = updated_0; @@ -432,7 +434,7 @@ impl RegExpPresentationResume { }, }), PresentationPhase::Flags(output) => Ok(RegExpPresentationStep::Primitive { - value, + value: runtime.into_jsvalue(value)?, resume: { let updated_0 = PresentationPhase::FlagsString(output); self.0.phase = updated_0; @@ -448,7 +450,9 @@ impl RegExpPresentationResume { let source = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpPresentationStep::Complete(Completion::Throw(value))); + return Ok(RegExpPresentationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let mut output = JsStringBuilder::new(source.len().saturating_add(2)); @@ -474,12 +478,14 @@ impl RegExpPresentationResume { let flags = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpPresentationStep::Complete(Completion::Throw(value))); + return Ok(RegExpPresentationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; output.push_js_string(&flags)?; Ok(RegExpPresentationStep::Complete(Completion::Return( - Value::String(output.finish()?), + runtime.into_jsvalue(Value::String(output.finish()?))?, ))) } PresentationPhase::Flag { index, mut output } => { @@ -489,7 +495,7 @@ impl RegExpPresentationResume { let index = index + 1; if index == FLAG_PROPERTIES.len() { return Ok(RegExpPresentationStep::Complete(Completion::Return( - Value::String(JsString::try_from_utf8(&output)?), + runtime.into_jsvalue(Value::String(JsString::try_from_utf8(&output)?))?, ))); } Ok(RegExpPresentationStep::Read { @@ -522,8 +528,8 @@ fn finish_presentation( runtime.get_property_in_realm(realm, &object, &key)?, )?, RegExpPresentationStep::Primitive { value, resume } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::String)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/regexp/replace.rs b/src/engine/builtins/regexp/replace.rs index d17ec074..cdab1b4e 100644 --- a/src/engine/builtins/regexp/replace.rs +++ b/src/engine/builtins/regexp/replace.rs @@ -14,7 +14,7 @@ use super::match_protocol::advance_string_index; use crate::engine::api::error::NativeErrorKind; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::builtins::native::NativeFunctionId; use super::super::replacement::{ @@ -27,7 +27,7 @@ use crate::engine::heap::{ use crate::engine::object::operations::InternalSetResult; use crate::engine::object::{CallableRef, ObjectRef, PropertyKey}; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::call::DirectCallTarget; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; use crate::engine::vm::{Completion, ToPrimitiveHint}; @@ -78,7 +78,7 @@ impl Runtime { return Ok(None); } let shape = state.heap.shape(object.shape)?; - let Some(last_index_slot) = shape.find(last_index.atom()) else { + let Some(last_index_slot) = shape.find(AtomIdx::from_raw(last_index.atom().raw())) else { return Ok(None); }; let last_index_slot = usize::try_from(last_index_slot) @@ -144,13 +144,15 @@ impl Runtime { let sticky = flags.contains(RegExpFlags::STICKY); let mut last_index = if global { if let Some(value) = self.set_regexp_last_index(realm, regexp, 0)? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } 0 } else if sticky { match self.native_to_length(realm, &standard.last_index)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } } else { 0 @@ -170,14 +172,14 @@ impl Runtime { ) { Ok(value) => value, Err(ExecError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory in regexp execution", )?)); } Err(ExecError::Interrupted) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "interrupted", @@ -200,7 +202,7 @@ impl Runtime { if (global || sticky) && let Some(value) = self.set_regexp_last_index(realm, regexp, 0)? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } break; }; @@ -236,7 +238,7 @@ impl Runtime { )?; let status = match status { Ok(status) => status, - Err(value) => return Ok(Completion::Throw(value)), + Err(value) => return Ok(Completion::Throw(self.into_jsvalue(value)?)), }; if matches!(status, SubstitutionStatus::BufferFailed) || output.error().is_some() { return self.complete_regexp_replacement_buffer(realm, output); @@ -249,7 +251,7 @@ impl Runtime { RuntimeError::Invariant("RegExp match end exceeded signed String range") })?; if let Some(value) = self.set_regexp_last_index(realm, regexp, end)? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } } break; @@ -273,8 +275,10 @@ impl Runtime { output: ReplacementStringBuffer, ) -> Result { match self.finish_replacement_buffer(realm, output)? { - NativeConversion::Value(value) => Ok(Completion::Return(Value::String(value))), - NativeConversion::Throw(value) => Ok(Completion::Throw(value)), + NativeConversion::Value(value) => { + Ok(Completion::Return(self.into_jsvalue(Value::String(value))?)) + } + NativeConversion::Throw(value) => Ok(Completion::Throw(self.into_jsvalue(value)?)), } } } @@ -326,7 +330,7 @@ fn raw_regexp_property_slot( return Ok(None); } let shape = heap.shape(object.shape)?; - if let Some(index) = shape.find(atom) { + if let Some(index) = shape.find(AtomIdx::from_raw(atom.raw())) { let index = usize::try_from(index) .map_err(|_| RuntimeError::Invariant("shape index does not fit usize"))?; return object @@ -386,6 +390,7 @@ impl std::ops::DerefMut for RegExpReplaceResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct RegExpReplaceResumeState { + runtime: Runtime, step_pending: RegExpReplaceStepPending, realm: ContextId, phase: ReplacePhase, @@ -394,6 +399,30 @@ pub(crate) struct RegExpReplaceResumeState { matched: Option, named: Option, } +impl Drop for RegExpReplaceResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.step_pending.value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.step_pending.receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.step_pending.arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(value) = self.step_pending.regexp.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.step_pending.input.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} struct ReplaceState { regexp: ObjectRef, replacement_value: Option, @@ -493,25 +522,18 @@ impl RegExpReplaceStep { "RegExp @@replace did not receive a generic invocation", )); }; + let regexp = runtime.root_value(regexp)?; let Value::Object(regexp) = regexp else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; - let mut input = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp @@replace input argv was not padded", - ))? - .clone(); - let mut replacement = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "RegExp @@replace replacement argv was not padded", - ))? - .clone(); + let mut input = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("RegExp @@replace input argv was not padded"), + )?)?; + let mut replacement = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("RegExp @@replace replacement argv was not padded"), + )?)?; // Preserve the outer buffer reservation/error latch even when the // standard kernel subsequently uses its own second buffer. let output = ReplacementStringBuffer::new(0); @@ -521,14 +543,18 @@ impl RegExpReplaceStep { input = match converted_string(runtime, realm, input)? { NativeConversion::Value(value) => Value::String(value), NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !matches!(replacement, Value::Object(_)) { replacement = match converted_string(runtime, realm, replacement)? { NativeConversion::Value(value) => Value::String(value), NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; } @@ -536,10 +562,10 @@ impl RegExpReplaceStep { // Already converted strings and a guarded standard RegExp complete // through the existing kernel; no continuation owner is needed. if let (Value::String(source), Value::String(text)) = (&input, &replacement) - && let Some(standard) = runtime.standard_regexp_replace(regexp)? + && let Some(standard) = runtime.standard_regexp_replace(®exp)? { return Ok(Self::Complete(runtime.call_standard_regexp_replace( - realm, regexp, source, text, standard, + realm, ®exp, source, text, standard, )?)); } #[cfg(feature = "profiling")] @@ -547,6 +573,7 @@ impl RegExpReplaceStep { "regexpreplace_resident_allocated", ); RegExpReplaceResume(Box::new(RegExpReplaceResumeState { + runtime: runtime.clone(), step_pending: RegExpReplaceStepPending::default(), realm, phase: ReplacePhase::Input, @@ -614,7 +641,7 @@ impl RegExpReplaceResume { message: &str, ) -> Result { Ok(ReplaceAction::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, kind, message)?, + runtime.new_native_error_jsvalue(self.0.realm, kind, message)?, ))) } fn primitive( @@ -661,7 +688,7 @@ impl RegExpReplaceResume { crate::engine::api::profiling::record_owned_execution_event( "regexpreplace_primitive_local", ); - self.advance(runtime, Completion::Return(value))? + self.advance(runtime, Completion::Return(runtime.into_jsvalue(value)?))? } ReplaceAction::Read { target, key } => { let object = self.read_object(target); @@ -674,7 +701,7 @@ impl RegExpReplaceResume { ); self.advance( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), )? } read => { @@ -715,21 +742,30 @@ impl RegExpReplaceResume { } } ReplaceAction::Primitive { value, hint } => { - return Ok(RegExpReplaceStep::make_primitive(value, hint, self)); + return Ok(RegExpReplaceStep::make_primitive( + runtime.into_jsvalue(value)?, + hint, + self, + )); } ReplaceAction::Call { target, receiver, arguments, } => { + let receiver = runtime.into_jsvalue(receiver)?; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; return Ok(RegExpReplaceStep::make_call( target, receiver, arguments, self, )); } ReplaceAction::Exec => { return Ok(RegExpReplaceStep::make_exec( - Value::Object(self.0.state.regexp.clone()), - Value::String(self.source().clone()), + runtime.into_jsvalue(Value::Object(self.0.state.regexp.clone()))?, + runtime.into_jsvalue(Value::String(self.source().clone()))?, self, )); } @@ -963,7 +999,9 @@ impl RegExpReplaceResume { .expect("replacement named cursor disappeared"); match runtime.finish_replacement_buffer(self.0.realm, state.buffer)? { NativeConversion::Value(value) => self.append_result(runtime, value), - NativeConversion::Throw(value) => Ok(ReplaceAction::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))), } } pub(crate) fn set( @@ -982,7 +1020,9 @@ impl RegExpReplaceResume { let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } match self.0.phase { ReplacePhase::InitialSet | ReplacePhase::AdvancedSet => self.execute(runtime), @@ -1005,7 +1045,7 @@ impl RegExpReplaceResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(ReplaceAction::Complete(Completion::Throw(value))); } @@ -1016,7 +1056,9 @@ impl RegExpReplaceResume { let source = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let replacement = self @@ -1046,7 +1088,9 @@ impl RegExpReplaceResume { self.0.state.replacement = Some(match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }); self.prepared(runtime) @@ -1058,7 +1102,9 @@ impl RegExpReplaceResume { let flags = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0.state.global = flags.utf16_units().any(|unit| unit == u16::from(b'g')); @@ -1107,7 +1153,9 @@ impl RegExpReplaceResume { let matched = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if matched.is_empty() { @@ -1136,7 +1184,9 @@ impl RegExpReplaceResume { let current = match runtime.native_to_length(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let next = advance_string_index(self.source(), current, self.0.state.unicode); @@ -1154,7 +1204,9 @@ impl RegExpReplaceResume { let count = match runtime.native_to_number(realm, &value)? { NativeConversion::Value(value) => Runtime::to_uint32_number(value), NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0 @@ -1180,7 +1232,9 @@ impl RegExpReplaceResume { let matched = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.0 @@ -1206,7 +1260,9 @@ impl RegExpReplaceResume { let position = match runtime.native_to_length(realm, &value)? { NativeConversion::Value(value) => value.min(self.source().len() as u64), NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let state = self @@ -1237,7 +1293,9 @@ impl RegExpReplaceResume { let capture = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.capture(runtime, Value::String(capture)) @@ -1294,7 +1352,9 @@ impl RegExpReplaceResume { match runtime.native_to_object(realm, value)? { NativeConversion::Value(value) => Some(value), NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } } }; @@ -1313,7 +1373,9 @@ impl RegExpReplaceResume { let text = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.append_result(runtime, text) @@ -1337,7 +1399,9 @@ impl RegExpReplaceResume { let text = match converted_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ReplaceAction::Complete(Completion::Throw(value))); + return Ok(ReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let named = self @@ -1411,7 +1475,9 @@ fn finish_replace( Completion::Return(_) => { NativeConversion::Value(InternalSetResult::Accepted) } - Completion::Throw(value) => NativeConversion::Throw(value), + Completion::Throw(value) => NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ), }; } SetStep::Complete(action) => break local_set_result(action)?, @@ -1426,10 +1492,12 @@ fn finish_replace( let key = resume.take_preparedread_key(); { let result = match runtime.finish_prepared_read(realm, &key, read)? { - NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) + NativeConversion::Value(value) => Completion::Return( + runtime.into_jsvalue(value.unwrap_or(Value::Undefined))?, + ), + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) } - NativeConversion::Throw(value) => Completion::Throw(value), }; resume.resume(runtime, result)? } @@ -1438,8 +1506,8 @@ fn finish_replace( let value = resume.take_primitive_value(); let hint = resume.take_primitive_hint(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; @@ -1448,8 +1516,12 @@ fn finish_replace( } RegExpReplaceStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -1463,8 +1535,8 @@ fn finish_replace( } } RegExpReplaceStep::Exec { mut resume } => { - let regexp = resume.take_exec_regexp(); - let input = resume.take_exec_input(); + let regexp = runtime.root_and_release_jsvalue(resume.take_exec_regexp())?; + let input = runtime.root_and_release_jsvalue(resume.take_exec_input())?; resume.resume(runtime, runtime.regexp_exec_abstract(realm, regexp, input)?)? } }; @@ -1520,13 +1592,17 @@ mod tests { // The original direct-matcher guard intentionally rejects lazy exec. assert!(runtime.standard_regexp_replace(®exp).unwrap().is_none()); let invocation = NativeInvocation::Call { - this_value: Value::Object(regexp), + this_value: runtime.into_jsvalue(Value::Object(regexp)).unwrap(), }; let arguments = NativeArguments { actual_arg_count: 2, readable: vec![ - Value::String(JsString::from_static("aa")), - Value::String(JsString::from_static("b")), + runtime + .into_jsvalue(Value::String(JsString::from_static("aa"))) + .unwrap(), + runtime + .into_jsvalue(Value::String(JsString::from_static("b"))) + .unwrap(), ], }; let step = @@ -1539,9 +1615,13 @@ mod tests { )); // Consuming the already-selected intrinsic must not probe flags again. context.eval("Object.defineProperty(coldReplace, 'flags', { get() { throw 'repeated flags'; } });").unwrap(); + let Completion::Return(value) = finish_replace(&runtime, context.realm, step).unwrap() + else { + panic!("standard replace did not return") + }; assert_eq!( - finish_replace(&runtime, context.realm, step).unwrap(), - Completion::Return(Value::String(JsString::from_static("bb"))) + runtime.root_and_release_jsvalue(value).unwrap(), + Value::String(JsString::from_static("bb")) ); assert_eq!( context.eval("coldReplace.lastIndex").unwrap(), @@ -1562,11 +1642,16 @@ mod tests { }; let callback_id = function.object_id(); let invocation = NativeInvocation::Call { - this_value: Value::Object(regexp), + this_value: runtime.into_jsvalue(Value::Object(regexp)).unwrap(), }; let arguments = NativeArguments { actual_arg_count: 2, - readable: vec![Value::String(JsString::from_static("a")), callback], + readable: vec![ + runtime + .into_jsvalue(Value::String(JsString::from_static("a"))) + .unwrap(), + runtime.into_jsvalue(callback).unwrap(), + ], }; // Primitive input/flags now complete locally. Pause at actual exec, // then at a selected result getter so abandonment still owns the @@ -1576,17 +1661,27 @@ mod tests { else { panic!("expected exec request") }; - drop(resume.take_exec_regexp()); - drop(resume.take_exec_input()); + runtime.release_jsvalue(resume.take_exec_regexp()).unwrap(); + runtime.release_jsvalue(resume.take_exec_input()).unwrap(); let resident_address = &*resume.0 as *const RegExpReplaceResumeState; - drop(invocation); - drop(arguments); + { + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + } let Value::Object(result) = context.eval("({get length(){return 1;}})").unwrap() else { panic!("result object") }; let result_id = result.object_id(); let RegExpReplaceStep::PreparedRead { mut resume } = resume - .resume(&runtime, Completion::Return(Value::Object(result))) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(Value::Object(result)).unwrap()), + ) .unwrap() else { panic!("expected selected result length getter") @@ -1617,13 +1712,13 @@ pub(crate) struct RegExpReplaceStepPending { step: Option>, read: Option, key: Option, - value: Option, + value: Option, hint: Option, target: Option, - receiver: Option, - arguments: Option>, - regexp: Option, - input: Option, + receiver: Option, + arguments: Option>, + regexp: Option, + input: Option, } impl RegExpReplaceStep { pub(crate) fn make_preparedset( @@ -1643,7 +1738,7 @@ impl RegExpReplaceStep { Self::PreparedRead { resume } } pub(crate) fn make_primitive( - value: Value, + value: JsValue, hint: ToPrimitiveHint, mut resume: RegExpReplaceResume, ) -> Self { @@ -1653,8 +1748,8 @@ impl RegExpReplaceStep { } pub(crate) fn make_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: RegExpReplaceResume, ) -> Self { resume.0.step_pending.target = Some(target); @@ -1662,7 +1757,11 @@ impl RegExpReplaceStep { resume.0.step_pending.arguments = Some(arguments); Self::Call { resume } } - pub(crate) fn make_exec(regexp: Value, input: Value, mut resume: RegExpReplaceResume) -> Self { + pub(crate) fn make_exec( + regexp: JsValue, + input: JsValue, + mut resume: RegExpReplaceResume, + ) -> Self { resume.0.step_pending.regexp = Some(regexp); resume.0.step_pending.input = Some(input); Self::Exec { resume } @@ -1692,7 +1791,7 @@ impl RegExpReplaceResume { .expect("RegExpReplaceStep::PreparedRead lost key") } - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -1714,14 +1813,14 @@ impl RegExpReplaceResume { .take() .expect("RegExpReplaceStep::Call lost target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .step_pending .receiver .take() .expect("RegExpReplaceStep::Call lost receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .step_pending .arguments @@ -1729,14 +1828,14 @@ impl RegExpReplaceResume { .expect("RegExpReplaceStep::Call lost arguments") } - pub(crate) fn take_exec_regexp(&mut self) -> Value { + pub(crate) fn take_exec_regexp(&mut self) -> JsValue { self.0 .step_pending .regexp .take() .expect("RegExpReplaceStep::Exec lost regexp") } - pub(crate) fn take_exec_input(&mut self) -> Value { + pub(crate) fn take_exec_input(&mut self) -> JsValue { self.0 .step_pending .input diff --git a/src/engine/builtins/regexp/result.rs b/src/engine/builtins/regexp/result.rs index f55b5d9d..6869a396 100644 --- a/src/engine/builtins/regexp/result.rs +++ b/src/engine/builtins/regexp/result.rs @@ -2,7 +2,7 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::heap::{ContextId, ObjectData, PropertySlot}; use crate::engine::object::shape::{PropertyFlags, ShapeEntry}; use std::collections::HashMap; @@ -170,7 +170,7 @@ impl Runtime { let entries = names .iter() .map(|&atom| ShapeEntry { - atom, + atom: AtomIdx::from_raw(atom.raw()), flags: PropertyFlags::data(true, true, true), }) .collect::>(); @@ -193,6 +193,15 @@ impl Runtime { .map(PropertySlot::Data) }) .collect::, _>>()?; + // The object retains its own copy edges inside the allocation, so the + // conversions' producer edges are released on every exit below. + let conversion_probes = slots + .iter() + .filter_map(|slot| match slot { + PropertySlot::Data(raw) => Some(raw.clone()), + _ => None, + }) + .collect::>(); let id = { let mut state = self.0.state.borrow_mut(); let atoms = state.retain_slot_atoms(&slots)?; @@ -203,10 +212,17 @@ impl Runtime { Ok(id) => id, Err(error) => { state.release_atoms(atoms)?; + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } return Err(error.into()); } } }; + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } Ok(ObjectRef::from_owned_handle(self.clone(), id)) } @@ -225,8 +241,11 @@ impl Runtime { .ok_or(RuntimeError::Invariant("RegExp result layouts missing"))?[layout]; let mut slots = Vec::with_capacity(properties.len() + 1); slots.push(PropertySlot::Data(crate::engine::heap::RawValue::Int(0))); + let mut conversion_probes = Vec::new(); for value in &properties { - slots.push(PropertySlot::Data(self.raw_property_value(value)?)); + let raw = self.raw_property_value(value)?; + conversion_probes.push(raw.clone()); + slots.push(PropertySlot::Data(raw)); } let id = { let mut state = self.0.state.borrow_mut(); @@ -235,10 +254,17 @@ impl Runtime { Ok(id) => id, Err(error) => { state.release_atoms(atoms)?; + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } return Err(error.into()); } } }; + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } let result = ObjectRef::from_owned_handle(self.clone(), id); for value in captures { self.append_fresh_array_value(&result, value)?; diff --git a/src/engine/builtins/regexp/search.rs b/src/engine/builtins/regexp/search.rs index b65d5406..f41ace85 100644 --- a/src/engine/builtins/regexp/search.rs +++ b/src/engine/builtins/regexp/search.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, operations::InternalSetResult}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -56,39 +56,41 @@ impl RegExpSearchStep { "RegExp @@search did not receive a generic invocation", )); }; + let this_value = runtime.root_value(this_value)?; let Value::Object(regexp) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; Ok(Self::make_primitive( - arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp @@search input argv was not padded", - ))? - .clone(), + runtime.dup_jsvalue(arguments.readable.first().ok_or(RuntimeError::Invariant( + "RegExp @@search input argv was not padded", + ))?)?, RegExpSearchResume(Box::new(RegExpSearchResumeState { step_pending: RegExpSearchStepPending::default(), realm, - regexp: regexp.clone(), + regexp, phase: SearchPhase::Input, })), )) } } impl RegExpSearchResume { - fn execute(mut self, input: JsString, previous: Value) -> RegExpSearchStep { - RegExpSearchStep::make_exec( - Value::Object(self.0.regexp.clone()), - Value::String(input), + fn execute( + mut self, + runtime: &Runtime, + input: JsString, + previous: Value, + ) -> Result { + Ok(RegExpSearchStep::make_exec( + runtime.into_jsvalue(Value::Object(self.0.regexp.clone()))?, + runtime.into_jsvalue(Value::String(input))?, { let updated_0 = SearchPhase::Exec(previous); self.0.phase = updated_0; self }, - ) + )) } fn result( mut self, @@ -96,9 +98,9 @@ impl RegExpSearchResume { result: Value, ) -> Result { match result { - Value::Null => Ok(RegExpSearchStep::Complete(Completion::Return(Value::Int( - -1, - )))), + Value::Null => Ok(RegExpSearchStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Int(-1))?, + ))), Value::Object(result) => Ok(RegExpSearchStep::make_read( result, runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Index)?, @@ -120,7 +122,7 @@ impl RegExpSearchResume { ) -> Result { // Both RegExpExec and the post-exec Get throw without restoration. let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpSearchStep::Complete(Completion::Throw(value))); } @@ -135,7 +137,9 @@ impl RegExpSearchResume { let input = match runtime.native_to_js_string(self.0.realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSearchStep::Complete(Completion::Throw(value))); + return Ok(RegExpSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(RegExpSearchStep::make_read( @@ -151,19 +155,19 @@ impl RegExpSearchResume { } SearchPhase::Previous(input) => { if value.same_value(&Value::Int(0)) { - Ok({ + { let updated_0 = SearchPhase::Index; self.0.phase = updated_0; self } - .execute(input, value)) + .execute(runtime, input, value) } else { Ok(RegExpSearchStep::make_set( self.0.regexp.clone(), runtime.pinned_property_key( crate::engine::atom::pinned::PinnedAtom::LastIndex, )?, - Value::Int(0), + runtime.into_jsvalue(Value::Int(0))?, { let updated_0 = SearchPhase::InitialSet { input, @@ -201,7 +205,7 @@ impl RegExpSearchResume { runtime.pinned_property_key( crate::engine::atom::pinned::PinnedAtom::LastIndex, )?, - previous, + runtime.into_jsvalue(previous)?, { let updated_0 = SearchPhase::Restored(result); self.0.phase = updated_0; @@ -210,7 +214,9 @@ impl RegExpSearchResume { )) } } - SearchPhase::Index => Ok(RegExpSearchStep::Complete(Completion::Return(value))), + SearchPhase::Index => Ok(RegExpSearchStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), _ => Err(RuntimeError::Invariant( "RegExp search Set received an untyped reply", )), @@ -224,15 +230,17 @@ impl RegExpSearchResume { let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; if let Some(value) = runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - return Ok(RegExpSearchStep::Complete(Completion::Throw(value))); + return Ok(RegExpSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } match self.0.phase { - SearchPhase::InitialSet { input, previous } => Ok({ + SearchPhase::InitialSet { input, previous } => { let updated_0 = SearchPhase::Index; self.0.phase = updated_0; self } - .execute(input, previous)), + .execute(runtime, input, previous), SearchPhase::Restored(result) => { let updated_0 = SearchPhase::Index; self.0.phase = updated_0; @@ -259,8 +267,8 @@ impl Runtime { RegExpSearchStep::Primitive { mut resume } => { let value = resume.take_primitive_value(); { - let result = if matches!(value, Value::Object(_)) { - self.to_primitive(realm, value, ToPrimitiveHint::String)? + let result = if matches!(value, JsValue::Object(_)) { + self.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? } else { Completion::Return(value) }; @@ -273,14 +281,14 @@ impl Runtime { resume.resume(self, self.get_property_in_realm(realm, &object, &key)?)? } RegExpSearchStep::Exec { mut resume } => { - let regexp = resume.take_exec_regexp(); - let input = resume.take_exec_input(); + let regexp = self.root_and_release_jsvalue(resume.take_exec_regexp())?; + let input = self.root_and_release_jsvalue(resume.take_exec_input())?; resume.resume(self, self.regexp_exec_abstract(realm, regexp, input)?)? } RegExpSearchStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = self.root_and_release_jsvalue(resume.take_set_value())?; resume.set( self, self.internal_set( @@ -299,14 +307,14 @@ impl Runtime { #[derive(Default)] pub(crate) struct RegExpSearchStepPending { - value: Option, + value: Option, object: Option, key: Option, - regexp: Option, - input: Option, + regexp: Option, + input: Option, } impl RegExpSearchStep { - pub(crate) fn make_primitive(value: Value, mut resume: RegExpSearchResume) -> Self { + pub(crate) fn make_primitive(value: JsValue, mut resume: RegExpSearchResume) -> Self { resume.0.step_pending.value = Some(value); Self::Primitive { resume } } @@ -322,7 +330,7 @@ impl RegExpSearchStep { pub(crate) fn make_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: RegExpSearchResume, ) -> Self { resume.0.step_pending.object = Some(object); @@ -330,14 +338,18 @@ impl RegExpSearchStep { resume.0.step_pending.value = Some(value); Self::Set { resume } } - pub(crate) fn make_exec(regexp: Value, input: Value, mut resume: RegExpSearchResume) -> Self { + pub(crate) fn make_exec( + regexp: JsValue, + input: JsValue, + mut resume: RegExpSearchResume, + ) -> Self { resume.0.step_pending.regexp = Some(regexp); resume.0.step_pending.input = Some(input); Self::Exec { resume } } } impl RegExpSearchResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -374,7 +386,7 @@ impl RegExpSearchResume { .take() .expect("RegExpSearchStep::Set lost key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -382,14 +394,14 @@ impl RegExpSearchResume { .expect("RegExpSearchStep::Set lost value") } - pub(crate) fn take_exec_regexp(&mut self) -> Value { + pub(crate) fn take_exec_regexp(&mut self) -> JsValue { self.0 .step_pending .regexp .take() .expect("RegExpSearchStep::Exec lost regexp") } - pub(crate) fn take_exec_input(&mut self) -> Value { + pub(crate) fn take_exec_input(&mut self) -> JsValue { self.0 .step_pending .input diff --git a/src/engine/builtins/regexp/species.rs b/src/engine/builtins/regexp/species.rs index 9326fac6..f32b71f1 100644 --- a/src/engine/builtins/regexp/species.rs +++ b/src/engine/builtins/regexp/species.rs @@ -62,9 +62,11 @@ impl RegExpSpeciesResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { - return Ok(RegExpSpeciesStep::Complete(NativeConversion::Throw(value))); + return Ok(RegExpSpeciesStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } }; if self.0.species { diff --git a/src/engine/builtins/regexp/split.rs b/src/engine/builtins/regexp/split.rs index 7de5768e..1b353cdc 100644 --- a/src/engine/builtins/regexp/split.rs +++ b/src/engine/builtins/regexp/split.rs @@ -8,7 +8,7 @@ use crate::engine::heap::ContextId; use crate::engine::object::{ObjectRef, PropertyKey, operations::InternalSetResult}; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::call::{ConstructorRef, NativeArguments, NativeInvocation}; use crate::engine::vm::{Completion, ToPrimitiveHint}; @@ -142,7 +142,9 @@ struct SplitState { } impl SplitState { fn complete(self) -> RegExpSplitStep { - RegExpSplitStep::Complete(Completion::Return(Value::Object(self.result))) + RegExpSplitStep::Complete(Completion::Return(JsValue::Object( + self.result.into_handle(), + ))) } fn append(&mut self, runtime: &Runtime, value: Value) -> Result<(), RuntimeError> { runtime.append_regexp_split_value(&self.result, &mut self.length, value) @@ -175,7 +177,7 @@ impl SplitState { Ok(RegExpSplitStep::make_set( self.splitter.clone(), runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?, - value, + runtime.into_jsvalue(value)?, RegExpSplitResume(Box::new(RegExpSplitResumeState { step_pending: RegExpSplitStepPending::default(), realm, @@ -183,10 +185,15 @@ impl SplitState { })), )) } - fn execute(self, realm: ContextId, empty: bool) -> RegExpSplitStep { - RegExpSplitStep::make_exec( - Value::Object(self.splitter.clone()), - Value::String(self.input.clone()), + fn execute( + self, + runtime: &Runtime, + realm: ContextId, + empty: bool, + ) -> Result { + Ok(RegExpSplitStep::make_exec( + runtime.into_jsvalue(Value::Object(self.splitter.clone()))?, + runtime.into_jsvalue(Value::String(self.input.clone()))?, RegExpSplitResume(Box::new(RegExpSplitResumeState { step_pending: RegExpSplitStepPending::default(), realm, @@ -196,7 +203,7 @@ impl SplitState { Phase::Exec(self) }, })), - ) + )) } fn captures( mut self, @@ -238,25 +245,18 @@ impl RegExpSplitStep { "RegExp @@split did not receive a generic invocation", )); }; + let this_value = runtime.root_value(this_value)?; let Value::Object(regexp) = this_value else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not an object")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not an object")?, ))); }; - let input = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "RegExp @@split input argv was not padded", - ))? - .clone(); - let limit = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "RegExp @@split limit argv was not padded", - ))? - .clone(); + let input = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("RegExp @@split input argv was not padded"), + )?)?; + let limit = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("RegExp @@split limit argv was not padded"), + )?)?; Ok(Self::make_primitive( input, ToPrimitiveHint::String, @@ -280,7 +280,9 @@ impl RegExpSplitResume { let constructor = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); + return Ok(RegExpSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let Phase::Species { @@ -316,8 +318,8 @@ impl RegExpSplitResume { let key = runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; let result = match runtime.finish_set_property_or_throw(self.0.realm, &key, result)? { - Some(value) => Completion::Throw(value), - None => Completion::Return(Value::Undefined), + Some(value) => Completion::Throw(runtime.into_jsvalue(value)?), + None => Completion::Return(JsValue::Undefined), }; self.resume(runtime, result) } @@ -327,7 +329,7 @@ impl RegExpSplitResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); } @@ -338,7 +340,9 @@ impl RegExpSplitResume { let input = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); + return Ok(RegExpSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(RegExpSplitStep::make_species( @@ -360,7 +364,7 @@ impl RegExpSplitResume { limit, constructor, } => Ok(RegExpSplitStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::String, Self(Box::new(RegExpSplitResumeState { step_pending: RegExpSplitStepPending::default(), @@ -382,7 +386,9 @@ impl RegExpSplitResume { let flags = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); + return Ok(RegExpSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let unicode = flags @@ -396,15 +402,15 @@ impl RegExpSplitResume { let mut arguments = Vec::new(); if arguments.try_reserve_exact(2).is_err() { return Ok(RegExpSplitStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?, ))); } - arguments.push(Value::Object(regexp)); - arguments.push(Value::String(flags)); + arguments.push(runtime.into_jsvalue(Value::Object(regexp))?); + arguments.push(runtime.into_jsvalue(Value::String(flags))?); Ok(RegExpSplitStep::make_construct( constructor, arguments, @@ -443,7 +449,7 @@ impl RegExpSplitResume { return Self::after_limit(state, runtime, realm); } Ok(RegExpSplitStep::make_primitive( - limit, + runtime.into_jsvalue(limit)?, ToPrimitiveHint::Number, Self(Box::new(RegExpSplitResumeState { step_pending: RegExpSplitStepPending::default(), @@ -456,7 +462,9 @@ impl RegExpSplitResume { let number = match runtime.native_to_number(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); + return Ok(RegExpSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; state.limit = Runtime::to_uint32_number(number); @@ -477,7 +485,7 @@ impl RegExpSplitResume { } Ok(state.complete()) } - Phase::Set(state) => Ok(state.execute(realm, false)), + Phase::Set(state) => state.execute(runtime, realm, false), Phase::Exec(mut state) => match value { Value::Null => { state.advance()?; @@ -498,7 +506,7 @@ impl RegExpSplitResume { )), }, Phase::End { state, matched } => Ok(RegExpSplitStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::Number, Self(Box::new(RegExpSplitResumeState { step_pending: RegExpSplitStepPending::default(), @@ -510,7 +518,9 @@ impl RegExpSplitResume { let end = match runtime.native_to_length(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); + return Ok(RegExpSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let end = usize::try_from(end.min(state.input.len() as u64)) @@ -536,7 +546,7 @@ impl RegExpSplitResume { )) } Phase::Count { state, matched } => Ok(RegExpSplitStep::make_primitive( - value, + runtime.into_jsvalue(value)?, ToPrimitiveHint::Number, Self(Box::new(RegExpSplitResumeState { step_pending: RegExpSplitStepPending::default(), @@ -548,7 +558,9 @@ impl RegExpSplitResume { let count = match runtime.native_to_length(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(RegExpSplitStep::Complete(Completion::Throw(value))); + return Ok(RegExpSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; state.captures(runtime, realm, matched, 1, count) @@ -579,7 +591,7 @@ impl RegExpSplitResume { return Ok(state.complete()); } if state.input.is_empty() { - return Ok(state.execute(realm, true)); + return state.execute(runtime, realm, true); } // Preserve key allocation before the first observable splitter write. runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::LastIndex)?; @@ -599,8 +611,8 @@ fn finish( let value = resume.take_primitive_value(); let hint = resume.take_primitive_hint(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; @@ -621,7 +633,11 @@ fn finish( } RegExpSplitStep::Construct { mut resume } => { let constructor = resume.take_construct_constructor(); - let arguments = resume.take_construct_arguments(); + let arguments = resume + .take_construct_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.construct_constructor_internal( @@ -635,7 +651,7 @@ fn finish( RegExpSplitStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); + let value = runtime.root_and_release_jsvalue(resume.take_set_value())?; resume.set( runtime, runtime.internal_set( @@ -648,8 +664,8 @@ fn finish( )? } RegExpSplitStep::Exec { mut resume } => { - let regexp = resume.take_exec_regexp(); - let input = resume.take_exec_input(); + let regexp = runtime.root_and_release_jsvalue(resume.take_exec_regexp())?; + let input = runtime.root_and_release_jsvalue(resume.take_exec_input())?; resume.resume(runtime, runtime.regexp_exec_abstract(realm, regexp, input)?)? } } @@ -658,19 +674,19 @@ fn finish( #[derive(Default)] pub(crate) struct RegExpSplitStepPending { - value: Option, + value: Option, hint: Option, object: Option, key: Option, regexp: Option, constructor: Option, - arguments: Option>, - exec_regexp: Option, - input: Option, + arguments: Option>, + exec_regexp: Option, + input: Option, } impl RegExpSplitStep { pub(crate) fn make_primitive( - value: Value, + value: JsValue, hint: ToPrimitiveHint, mut resume: RegExpSplitResume, ) -> Self { @@ -693,7 +709,7 @@ impl RegExpSplitStep { } pub(crate) fn make_construct( constructor: ConstructorRef, - arguments: Vec, + arguments: Vec, mut resume: RegExpSplitResume, ) -> Self { resume.0.step_pending.constructor = Some(constructor); @@ -703,7 +719,7 @@ impl RegExpSplitStep { pub(crate) fn make_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: RegExpSplitResume, ) -> Self { resume.0.step_pending.object = Some(object); @@ -711,14 +727,18 @@ impl RegExpSplitStep { resume.0.step_pending.value = Some(value); Self::Set { resume } } - pub(crate) fn make_exec(regexp: Value, input: Value, mut resume: RegExpSplitResume) -> Self { + pub(crate) fn make_exec( + regexp: JsValue, + input: JsValue, + mut resume: RegExpSplitResume, + ) -> Self { resume.0.step_pending.exec_regexp = Some(regexp); resume.0.step_pending.input = Some(input); Self::Exec { resume } } } impl RegExpSplitResume { - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -763,7 +783,7 @@ impl RegExpSplitResume { .take() .expect("RegExpSplitStep::Construct lost constructor") } - pub(crate) fn take_construct_arguments(&mut self) -> Vec { + pub(crate) fn take_construct_arguments(&mut self) -> Vec { self.0 .step_pending .arguments @@ -785,7 +805,7 @@ impl RegExpSplitResume { .take() .expect("RegExpSplitStep::Set lost key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -793,14 +813,14 @@ impl RegExpSplitResume { .expect("RegExpSplitStep::Set lost value") } - pub(crate) fn take_exec_regexp(&mut self) -> Value { + pub(crate) fn take_exec_regexp(&mut self) -> JsValue { self.0 .step_pending .exec_regexp .take() .expect("RegExpSplitStep::Exec lost regexp") } - pub(crate) fn take_exec_input(&mut self) -> Value { + pub(crate) fn take_exec_input(&mut self) -> JsValue { self.0 .step_pending .input diff --git a/src/engine/builtins/replacement.rs b/src/engine/builtins/replacement.rs index 7a2658b4..95583bbc 100644 --- a/src/engine/builtins/replacement.rs +++ b/src/engine/builtins/replacement.rs @@ -208,9 +208,11 @@ impl Runtime { &key, )? { Completion::Return(value) => value, - Completion::Throw(value) => return Ok(Err(value)), + Completion::Throw(value) => { + return Ok(Err(self.root_and_release_jsvalue(value)?)); + } }; - match named_substitution_capture(buffer, capture) { + match named_substitution_capture(buffer, self.root_and_release_jsvalue(capture)?) { NamedSubstitutionCapture::Skip => continue, NamedSubstitutionCapture::Failed => { return Ok(Ok(SubstitutionStatus::BufferFailed)); diff --git a/src/engine/builtins/set.rs b/src/engine/builtins/set.rs index 890dfe76..511edd1a 100644 --- a/src/engine/builtins/set.rs +++ b/src/engine/builtins/set.rs @@ -19,7 +19,7 @@ use crate::engine::object::{ WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation, NativeInvokeOutcome}; @@ -99,13 +99,17 @@ impl Runtime { let values_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Values)?; let values = match self.get_property_in_realm(realm, &set_prototype, &values_key)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) => { + Completion::Return(value @ JsValue::Object(_)) => { + self.root_and_release_jsvalue(value)? + } + Completion::Return(value) => { + self.release_jsvalue(value)?; return Err(RuntimeError::Invariant( "Set.prototype.values was not callable during bootstrap", )); } - Completion::Throw(_) => { + Completion::Throw(value) => { + self.release_jsvalue(value)?; return Err(RuntimeError::Invariant( "Set.prototype.values initialization threw during bootstrap", )); @@ -295,7 +299,9 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.call_set_native_borrowed(realm, kind, &invocation, arguments) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.call_set_native_borrowed(realm, kind, invocation, arguments) + }) } pub(crate) fn call_set_native_borrowed( &self, @@ -341,7 +347,7 @@ impl Runtime { "Set species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } fn call_set_constructor( @@ -363,12 +369,12 @@ impl Runtime { ) } - fn set_receiver<'a>( + fn set_receiver( &self, realm: ContextId, - invocation: &'a NativeInvocation, + invocation: &NativeInvocation, getter: bool, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { let this_value = match (getter, invocation) { (false, NativeInvocation::Call { this_value }) | (true, NativeInvocation::Getter { this_value }) => this_value, @@ -378,13 +384,14 @@ impl Runtime { )); } }; - let Value::Object(object) = this_value else { + let JsValue::Object(id) = this_value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, NativeErrorKind::Type, "Set object expected", )?)); }; + let object = ObjectRef::from_borrowed_handle(self.clone(), *id)?; if !object.belongs_to(self) { return Err(RuntimeError::WrongRuntime("Set receiver")); } @@ -407,15 +414,19 @@ impl Runtime { Ok(NativeConversion::Value(object)) } - fn normalized_set_key(value: Value) -> Value { + fn normalized_set_key(value: JsValue) -> JsValue { match value { - Value::Float(0.0) => Value::Int(0), + JsValue::Float(0.0) => JsValue::Int(0), value => value, } } - fn find_set_record(&self, set: &ObjectRef, key: &Value) -> Result, RuntimeError> { - let raw_key = self.raw_property_value(key)?; + fn find_set_record( + &self, + set: &ObjectRef, + key: &JsValue, + ) -> Result, RuntimeError> { + let raw_key = key.as_raw(); Ok(self .0 .state @@ -424,36 +435,41 @@ impl Runtime { .set_find_record(set.object_id(), &raw_key)?) } - fn insert_set_record(&self, set: &ObjectRef, key: Value) -> Result { - self.validate_value_domain(&key, "Set value")?; + fn insert_set_record(&self, set: &ObjectRef, key: JsValue) -> Result { let key = Self::normalized_set_key(key); if self.find_set_record(set, &key)?.is_some() { + self.release_jsvalue(key)?; return Ok(false); } - let raw_key = self.raw_property_value(&key)?; + let raw_key = key.as_raw(); let mut state = self.0.state.borrow_mut(); let retained = state.retain_raw_value_atoms([&raw_key])?; let cleanup = match state.heap.set_insert_record(set.object_id(), raw_key) { Ok(cleanup) => cleanup, Err(error) => { state.release_atoms(retained)?; + drop(state); + self.release_jsvalue(key)?; return Err(error.into()); } }; state.apply_cleanup(cleanup)?; drop(state); - drop(key); + self.release_jsvalue(key)?; Ok(true) } - fn delete_set_record(&self, set: &ObjectRef, key: &Value) -> Result { - let key = Self::normalized_set_key(key.clone()); + fn delete_set_record(&self, set: &ObjectRef, key: JsValue) -> Result { + let key = Self::normalized_set_key(key); let Some(index) = self.find_set_record(set, &key)? else { + self.release_jsvalue(key)?; return Ok(false); }; let mut state = self.0.state.borrow_mut(); let cleanup = state.heap.set_delete_record(set.object_id(), index)?; state.apply_cleanup(cleanup)?; + drop(state); + self.release_jsvalue(key)?; Ok(true) } @@ -465,7 +481,7 @@ impl Runtime { &self, set: &ObjectRef, index: &mut usize, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { let record = self .0 .state @@ -480,14 +496,17 @@ impl Runtime { *index = record_index .checked_add(1) .ok_or(RuntimeError::Invariant("Set record index overflowed"))?; - Ok(Some((record_index, self.root_raw_value(&key)?))) + Ok(Some(( + record_index, + self.into_jsvalue(self.root_raw_value(&key)?)?, + ))) } fn next_live_set_value( &self, set: &ObjectRef, index: &mut usize, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { Ok(self .next_live_set_record(set, index)? .map(|(_, value)| value)) @@ -501,17 +520,15 @@ impl Runtime { ) -> Result { let set = match self.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Set.prototype.add value argv was not padded", - ))?; - self.insert_set_record(set, value)?; - Ok(Completion::Return(Value::Object(set.clone()))) + let value = self.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Set.prototype.add value argv was not padded"), + )?)?; + self.insert_set_record(&set, value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object(set))?)) } fn call_set_has( @@ -522,14 +539,17 @@ impl Runtime { ) -> Result { let set = match self.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let value = Self::normalized_set_key(arguments.readable.first().cloned().ok_or( - RuntimeError::Invariant("Set.prototype.has value argv was not padded"), - )?); - Ok(Completion::Return(Value::Bool( - self.find_set_record(set, &value)?.is_some(), - ))) + let value = + Self::normalized_set_key(self.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Set.prototype.has value argv was not padded"), + )?)?); + let has = self.find_set_record(&set, &value)?.is_some(); + self.release_jsvalue(value)?; + Ok(Completion::Return(JsValue::Bool(has))) } fn call_set_delete( @@ -540,17 +560,15 @@ impl Runtime { ) -> Result { let set = match self.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Set.prototype.delete value argv was not padded", - ))?; - Ok(Completion::Return(Value::Bool( - self.delete_set_record(set, &value)?, + let value = self.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("Set.prototype.delete value argv was not padded"), + )?)?; + Ok(Completion::Return(JsValue::Bool( + self.delete_set_record(&set, value)?, ))) } @@ -561,12 +579,14 @@ impl Runtime { ) -> Result { let set = match self.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let mut state = self.0.state.borrow_mut(); let cleanup = state.heap.set_clear(set.object_id())?; state.apply_cleanup(cleanup)?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } fn call_set_size( @@ -576,10 +596,12 @@ impl Runtime { ) -> Result { let set = match self.set_receiver(realm, invocation, true)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - Ok(Completion::Return(Value::number( - self.set_size_value(set)? as f64 + Ok(Completion::Return(JsValue::Int( + self.set_size_value(&set)? as i32 ))) } @@ -633,11 +655,13 @@ impl Runtime { ) -> Result { let set = match self.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - Ok(Completion::Return(Value::Object( - self.new_set_iterator(realm, set, kind)?, - ))) + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_set_iterator(realm, &set, kind)?, + ))?)) } pub(crate) fn call_set_iterator_next( @@ -647,9 +671,12 @@ impl Runtime { ) -> Result { match self.call_set_iterator_next_raw(realm, invocation)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object( + self.new_iterator_result(realm, value, done)?, + ))?)) + } } } @@ -663,9 +690,9 @@ impl Runtime { "Set Iterator next did not receive an iterator-next invocation", )); }; - let Value::Object(iterator) = this_value else { + let JsValue::Object(iterator_id) = this_value else { return Ok(NativeInvokeOutcome::Completion(Completion::Throw( - self.new_native_error( + self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "Set Iterator object expected", @@ -677,12 +704,12 @@ impl Runtime { .state .borrow_mut() .heap - .begin_set_iterator_next(iterator.object_id()); + .begin_set_iterator_next(iterator_id); let (set, mut index, kind) = match state { Ok(state) => state, Err(HeapError::Invariant(_)) => { return Ok(NativeInvokeOutcome::Completion(Completion::Throw( - self.new_native_error( + self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "Set Iterator object expected", @@ -693,7 +720,7 @@ impl Runtime { }; let Some(set_id) = set else { return Ok(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }); }; @@ -707,10 +734,10 @@ impl Runtime { .map(|(id, record)| (id, record.key.clone())); let Some((record_index, key)) = record else { let mut state = self.0.state.borrow_mut(); - let cleanup = state.heap.finish_set_iterator(iterator.object_id())?; + let cleanup = state.heap.finish_set_iterator(iterator_id)?; state.apply_cleanup(cleanup)?; return Ok(NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }); }; @@ -721,17 +748,21 @@ impl Runtime { .state .borrow_mut() .heap - .set_set_iterator_index(iterator.object_id(), index)?; + .set_set_iterator_index(iterator_id, index)?; self.0 .state .borrow_mut() .heap - .set_set_iterator_current(iterator.object_id(), record_index)?; + .set_set_iterator_current(iterator_id, record_index)?; let value = self.root_raw_value(&key)?; let value = match kind { - SetIteratorKind::Value => value, + SetIteratorKind::Value => self.into_jsvalue(value)?, SetIteratorKind::KeyAndValue => { - Value::Object(self.new_array_from_values(realm, vec![value.clone(), value])?) + let key = self.into_jsvalue(value)?; + let second = self.dup_jsvalue(&key)?; + self.into_jsvalue(Value::Object( + self.new_array_from_values_jsvalue(realm, vec![key, second])?, + ))? } }; Ok(NativeInvokeOutcome::IteratorNextRaw { value, done: false }) diff --git a/src/engine/builtins/set/callback.rs b/src/engine/builtins/set/callback.rs index 99938116..6bc56c47 100644 --- a/src/engine/builtins/set/callback.rs +++ b/src/engine/builtins/set/callback.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{CallableRef, ObjectRef}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -28,13 +28,31 @@ impl std::ops::DerefMut for EachResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct EachResumeState { + runtime: Runtime, pending_effect: EachStepPending, record: Option, set: ObjectRef, callback: CallableRef, - receiver: Value, + receiver: JsValue, index: usize, } +impl Drop for EachResumeState { + /// Release the internal edges the pending effect and resident receiver + /// still own when the request is abandoned. Consumption goes through + /// `Option::take`; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.pending_effect.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.pending_effect.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + let receiver = std::mem::replace(&mut self.receiver, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(receiver); + } +} impl EachStep { pub(crate) fn start( runtime: &Runtime, @@ -44,29 +62,35 @@ impl EachStep { ) -> Result { let set = match runtime.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; let value = arguments.readable.first().ok_or(RuntimeError::Invariant( "Set.prototype.forEach callback argv was not padded", ))?; let callback = match value { - Value::Object(object) => runtime.as_callable(object)?, + JsValue::Object(id) => { + runtime.as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)? + } _ => None, }; let Some(callback) = callback else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not a function")?, ))); }; EachResume(Box::new(EachResumeState { + runtime: runtime.clone(), pending_effect: EachStepPending::default(), set: set.clone(), callback, - receiver: arguments - .readable - .get(1) - .cloned() - .unwrap_or(Value::Undefined), + receiver: match arguments.readable.get(1) { + Some(value) => runtime.dup_jsvalue(value)?, + None => JsValue::Undefined, + }, index: 0, record: None, })) @@ -78,7 +102,7 @@ impl EachResume { let Some((record_index, value)) = runtime.next_live_set_record(&self.0.set, &mut self.0.index)? else { - return Ok(EachStep::Complete(Completion::Return(Value::Undefined))); + return Ok(EachStep::Complete(Completion::Return(JsValue::Undefined))); }; self.0.record = Some( runtime.push_active_collection_record(ActiveCollectionRecord::Set { @@ -88,8 +112,12 @@ impl EachResume { ); Ok(EachStep::request_call( self.0.callback.clone(), - self.0.receiver.clone(), - vec![value.clone(), value, Value::Object(self.0.set.clone())], + runtime.dup_jsvalue(&self.0.receiver)?, + vec![ + runtime.dup_jsvalue(&value)?, + value, + runtime.into_jsvalue(Value::Object(self.0.set.clone()))?, + ], self, )) } @@ -117,8 +145,12 @@ pub(crate) fn finish( EachStep::Complete(result) => return Ok(result), EachStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -131,14 +163,14 @@ pub(crate) fn finish( #[derive(Default)] struct EachStepPending { call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, } impl EachStep { pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: EachResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -155,14 +187,14 @@ impl EachResume { .take() .expect("EachStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("EachStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/set/operations.rs b/src/engine/builtins/set/operations.rs index c9102836..b71ef992 100644 --- a/src/engine/builtins/set/operations.rs +++ b/src/engine/builtins/set/operations.rs @@ -8,7 +8,7 @@ use crate::engine::{ }, heap::{ContextId, ObjectPayload}, object::{CallableRef, ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -62,17 +62,18 @@ const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct SetResumeState { pending_effect: SetStepPending, phase: Phase, + runtime: Runtime, realm: ContextId, kind: SetOperation, set: ObjectRef, - target: Value, + target: JsValue, size: i64, has: Option, keys: Option, result: Option, index: usize, - iterator: Value, - next: Value, + iterator: JsValue, + next: JsValue, } enum Phase { Size, @@ -85,7 +86,7 @@ enum Phase { Parse, Probe { record: Option, - value: Value, + value: JsValue, }, CloseMethod, CloseCall, @@ -100,47 +101,43 @@ impl SetStep { ) -> Result { let set = match runtime.set_receiver(realm, invocation, false)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } }; - let target = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Set method operand argv was not padded", - ))?; - if matches!(target, Value::Null | Value::Undefined) { - let base = if matches!(target, Value::Null) { + let target_ref = arguments.readable.first().ok_or(RuntimeError::Invariant( + "Set method operand argv was not padded", + ))?; + if matches!(target_ref, JsValue::Null | JsValue::Undefined) { + let base = if matches!(target_ref, JsValue::Null) { "null" } else { "undefined" }; return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!("cannot read property 'size' of {base}"), )?, ))); } - let genuine_size = if let Value::Object(object) = &target { - if !object.belongs_to(runtime) { - return Err(RuntimeError::WrongRuntime("set-like object")); - } + let genuine_size = if let JsValue::Object(id) = target_ref { let state = runtime.0.state.borrow(); - if matches!( - state.heap.object(object.object_id())?.payload, - ObjectPayload::Set { .. } - ) { - Some(state.heap.set_size(object.object_id())?) + if matches!(state.heap.object(*id)?.payload, ObjectPayload::Set { .. }) { + Some(state.heap.set_size(*id)?) } else { None } } else { None }; + let target = runtime.dup_jsvalue(target_ref)?; let mut resume = SetResume(Box::new(SetResumeState { pending_effect: SetStepPending::default(), + runtime: runtime.clone(), realm, kind, set: set.clone(), @@ -150,8 +147,8 @@ impl SetStep { keys: None, result: None, index: 0, - iterator: Value::Undefined, - next: Value::Undefined, + iterator: JsValue::Undefined, + next: JsValue::Undefined, phase: Phase::Size, })); if let Some(size) = genuine_size { @@ -165,9 +162,10 @@ impl SetStep { } } impl SetResume { - fn read(self, runtime: &Runtime, name: &str) -> Result { + fn read(mut self, runtime: &Runtime, name: &str) -> Result { + let target = std::mem::replace(&mut self.0.target, JsValue::Undefined); Ok(SetStep::request_read( - self.0.target.clone(), + target, runtime.intern_property_key(name)?, self, )) @@ -185,7 +183,9 @@ impl SetResume { } _ => Value::Object(self.result()?), }; - Ok(SetStep::Complete(Completion::Return(value))) + Ok(SetStep::Complete(Completion::Return( + self.0.runtime.into_jsvalue(value)?, + ))) } fn selected(mut self, runtime: &Runtime) -> Result { if matches!(self.0.kind, SetOperation::Difference) { @@ -195,7 +195,9 @@ impl SetResume { if matches!(self.0.kind, SetOperation::Subset) && size > self.0.size || matches!(self.0.kind, SetOperation::Superset) && size < self.0.size { - return Ok(SetStep::Complete(Completion::Return(Value::Bool(false)))); + return Ok(SetStep::Complete(Completion::Return( + self.0.runtime.into_jsvalue(Value::Bool(false))?, + ))); } let own = match self.0.kind { SetOperation::Subset => true, @@ -222,7 +224,7 @@ impl SetResume { .keys .clone() .ok_or(RuntimeError::Invariant("Set operation keys missing"))?, - self.0.target.clone(), + runtime.dup_jsvalue(&self.0.target)?, Vec::new(), self, )) @@ -242,7 +244,7 @@ impl SetResume { object: source.object_id(), index: record_index, }); - let arguments = vec![value.clone()]; + let arguments = vec![runtime.dup_jsvalue(&value)?]; self.0.phase = Phase::Probe { record: Some(record), value, @@ -252,25 +254,31 @@ impl SetResume { .has .clone() .ok_or(RuntimeError::Invariant("Set operation has missing"))?, - self.0.target.clone(), + runtime.dup_jsvalue(&self.0.target)?, arguments, self, )) } fn next_step(mut self, runtime: &Runtime) -> Result { let callable = match &self.0.next { - Value::Object(object) => runtime.as_callable(object)?, + JsValue::Object(id) => { + runtime.as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)? + } _ => None, }; let Some(callable) = callable else { return Ok(SetStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; self.0.phase = Phase::NextCall; Ok(SetStep::request_call( callable, - self.0.iterator.clone(), + runtime.dup_jsvalue(&self.0.iterator)?, Vec::new(), self, )) @@ -292,7 +300,9 @@ impl SetResume { if matches!(self.0.phase, Phase::CloseCall) || matches!(self.0.phase, Phase::CloseMethod) && matches!(reply, Completion::Throw(_)) { - return Ok(SetStep::Complete(Completion::Return(Value::Bool(false)))); + return Ok(SetStep::Complete(Completion::Return( + self.0.runtime.into_jsvalue(Value::Bool(false))?, + ))); } let value = match reply { Completion::Return(value) => value, @@ -306,22 +316,23 @@ impl SetResume { phase @ (Phase::Has | Phase::Keys) => { let has = matches!(phase, Phase::Has); let name = if has { "has" } else { "keys" }; - if matches!(value, Value::Undefined) { + if matches!(value, JsValue::Undefined) { return Ok(SetStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, &format!(".{name} is undefined"), )?, ))); } - let callable = match value { - Value::Object(ref object) => runtime.as_callable(object)?, + let callable = match &value { + JsValue::Object(id) => runtime + .as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)?, _ => None, }; let Some(callable) = callable else { return Ok(SetStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, &format!(".{name} is not a function"), @@ -338,21 +349,21 @@ impl SetResume { } } Phase::Iterator => { - if matches!(value, Value::Null | Value::Undefined) { - let base = if matches!(value, Value::Null) { + if matches!(value, JsValue::Null | JsValue::Undefined) { + let base = if matches!(value, JsValue::Null) { "null" } else { "undefined" }; return Ok(SetStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, &format!("cannot read property 'next' of {base}"), )?, ))); } - self.0.iterator = value.clone(); + self.0.iterator = runtime.dup_jsvalue(&value)?; self.0.phase = Phase::NextMethod; Ok(SetStep::request_read( value, @@ -374,36 +385,57 @@ impl SetResume { self.next_step(runtime) } Phase::Probe { value: item, .. } => { - let present = runtime.value_to_boolean(&value)?; + let present = runtime.value_to_boolean_jsvalue(&value)?; + let mut item = Some(item); match self.0.kind { SetOperation::Disjoint if present => { - return Ok(SetStep::Complete(Completion::Return(Value::Bool(false)))); + if let Some(item) = item.take() { + runtime.release_jsvalue(item)?; + } + return Ok(SetStep::Complete(Completion::Return( + self.0.runtime.into_jsvalue(Value::Bool(false))?, + ))); } SetOperation::Subset if !present => { - return Ok(SetStep::Complete(Completion::Return(Value::Bool(false)))); + if let Some(item) = item.take() { + runtime.release_jsvalue(item)?; + } + return Ok(SetStep::Complete(Completion::Return( + self.0.runtime.into_jsvalue(Value::Bool(false))?, + ))); } SetOperation::Intersection if present => { - runtime.insert_set_record(&self.result()?, item)?; + if let Some(item) = item.take() { + runtime.insert_set_record(&self.result()?, item)?; + } } SetOperation::Difference if present => { - runtime.delete_set_record(&self.result()?, &item)?; + if let Some(item) = item.take() { + runtime.delete_set_record(&self.result()?, item)?; + } } _ => {} } + if let Some(item) = item { + runtime.release_jsvalue(item)?; + } self.probe(runtime) } Phase::CloseMethod => { - let callable = match value { - Value::Object(ref object) => runtime.as_callable(object)?, + let callable = match &value { + JsValue::Object(id) => runtime + .as_callable(&ObjectRef::from_borrowed_handle(runtime.clone(), *id)?)?, _ => None, }; let Some(callable) = callable else { - return Ok(SetStep::Complete(Completion::Return(Value::Bool(false)))); + return Ok(SetStep::Complete(Completion::Return( + self.0.runtime.into_jsvalue(Value::Bool(false))?, + ))); }; self.0.phase = Phase::CloseCall; Ok(SetStep::request_call( callable, - self.0.iterator.clone(), + runtime.dup_jsvalue(&self.0.iterator)?, Vec::new(), self, )) @@ -421,12 +453,14 @@ impl SetResume { let size = match reply { NativeConversion::Value(size) => size, NativeConversion::Throw(value) => { - return Ok(SetStep::Complete(Completion::Throw(value))); + return Ok(SetStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if size.is_nan() { return Ok(SetStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Type, ".size is not a number", @@ -442,7 +476,7 @@ impl SetResume { }; if size < 0 { return Ok(SetStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, ".size must be positive", @@ -469,11 +503,11 @@ impl SetResume { match self.0.kind { SetOperation::Disjoint | SetOperation::Superset => { let present = runtime.find_set_record(&self.0.set, &value)?.is_some(); - drop(value); + runtime.release_jsvalue(value)?; if present == matches!(self.0.kind, SetOperation::Disjoint) { self.0.phase = Phase::CloseMethod; return Ok(SetStep::request_read( - self.0.iterator.clone(), + runtime.dup_jsvalue(&self.0.iterator)?, runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Return)?, self, @@ -486,11 +520,11 @@ impl SetResume { } } SetOperation::Difference => { - runtime.delete_set_record(&self.result()?, &value)?; + runtime.delete_set_record(&self.result()?, value)?; } SetOperation::SymmetricDifference => { if runtime.find_set_record(&self.0.set, &value)?.is_some() { - runtime.delete_set_record(&self.result()?, &value)?; + runtime.delete_set_record(&self.result()?, value)?; } else { runtime.insert_set_record(&self.result()?, value)?; } @@ -518,17 +552,25 @@ pub(crate) fn finish( let key = resume.take_read_key(); resume.resume( runtime, - runtime.get_value_property_in_realm(realm, receiver, &key)?, + runtime.get_value_property_in_realm( + realm, + runtime.root_and_release_jsvalue(receiver)?, + &key, + )?, )? } SetStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } SetStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.call_internal(realm, &callable, receiver, &arguments)?, @@ -551,28 +593,28 @@ pub(crate) fn finish( #[derive(Default)] struct SetStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, - number_value: Option, + number_value: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, parse_result: Option, } impl SetStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: SetResume) -> Self { + pub(crate) fn request_read(receiver: JsValue, key: PropertyKey, mut resume: SetResume) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } - pub(crate) fn request_number(value: Value, mut resume: SetResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: SetResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: SetResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -586,7 +628,7 @@ impl SetStep { } } impl SetResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -600,7 +642,7 @@ impl SetResume { .take() .expect("SetStep Read key") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value @@ -614,14 +656,14 @@ impl SetResume { .take() .expect("SetStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("SetStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/builtins/shared_array_buffer.rs b/src/engine/builtins/shared_array_buffer.rs index bc179cb2..93f3c3f0 100644 --- a/src/engine/builtins/shared_array_buffer.rs +++ b/src/engine/builtins/shared_array_buffer.rs @@ -21,7 +21,7 @@ use crate::engine::object::{ WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -212,7 +212,7 @@ impl Runtime { max_byte_length: Option, ) -> Result { if length > u64::from(MAX_SHARED_ARRAY_BUFFER_BYTE_LENGTH) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid array buffer length", @@ -221,7 +221,7 @@ impl Runtime { if max_byte_length .is_some_and(|maximum| maximum > u64::from(MAX_SHARED_ARRAY_BUFFER_BYTE_LENGTH)) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid max array buffer length", @@ -240,7 +240,7 @@ impl Runtime { let handle = match SharedBufferHandle::new(length, max_byte_length) { Ok(handle) => handle, Err(SharedMemoryError::Allocation) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", @@ -249,7 +249,7 @@ impl Runtime { Err(error) => return Err(shared_memory_runtime_error(error)), }; let object = self.new_shared_array_buffer_from_handle(&prototype, handle)?; - Ok(Completion::Return(Value::Object(object))) + Ok(Completion::Return(JsValue::Object(object.into_handle()))) } pub(in crate::engine::builtins) fn call_shared_array_buffer_species( @@ -261,7 +261,7 @@ impl Runtime { "SharedArrayBuffer species did not receive a getter invocation", )); }; - Ok(Completion::Return(this_value.clone())) + Ok(Completion::Return(self.dup_jsvalue(this_value)?)) } pub(in crate::engine::builtins) fn call_shared_array_buffer_getter( @@ -275,9 +275,12 @@ impl Runtime { "SharedArrayBuffer prototype getter received a non-getter invocation", )); }; - let object = match self.require_shared_array_buffer_borrowed(realm, this_value)? { + let this_value = self.root_value(this_value)?; + let object = match self.require_shared_array_buffer_borrowed(realm, &this_value)? { NativeConversion::Value(object) => object, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; let snapshot = self.shared_array_buffer_snapshot(object)?; let value = match kind { @@ -301,7 +304,7 @@ impl Runtime { )); } }; - Ok(Completion::Return(value)) + Ok(Completion::Return(self.into_jsvalue(value)?)) } fn call_shared_array_buffer_grow( @@ -329,7 +332,7 @@ impl Runtime { ) -> Result { let current = self.shared_array_buffer_snapshot(&object)?; let Some(maximum) = current.max_byte_length else { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "array buffer is not resizable", @@ -339,7 +342,7 @@ impl Runtime { || new_length > i64::from(maximum) || new_length < i64::from(current.byte_length) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid array buffer length", @@ -353,7 +356,7 @@ impl Runtime { .borrow_mut() .heap .grow_shared_array_buffer(object.object_id(), new_length)?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } fn call_shared_array_buffer_slice( @@ -421,7 +424,7 @@ impl Runtime { new_length: u32, ) -> Result { if target.object_id() == source.object_id() { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot use identical ArrayBuffer", @@ -430,7 +433,7 @@ impl Runtime { let target_snapshot = match self.shared_array_buffer_snapshot_if_branded(&target)? { Some(snapshot) => snapshot, None => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "SharedArrayBuffer object expected", @@ -438,7 +441,7 @@ impl Runtime { } }; if target_snapshot.byte_length < new_length { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "new ArrayBuffer is too small", @@ -456,7 +459,7 @@ impl Runtime { )); }; if end > source_snapshot.byte_length { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "ArrayBuffer is detached", @@ -477,7 +480,7 @@ impl Runtime { target_handle .copy_range_from(&source_handle, start, 0, new_length) .map_err(shared_memory_runtime_error)?; - Ok(Completion::Return(Value::Object(target))) + Ok(Completion::Return(JsValue::Object(target.into_handle()))) } pub(in crate::engine::builtins) fn shared_array_buffer_default_prototype( diff --git a/src/engine/builtins/string.rs b/src/engine/builtins/string.rs index 5c23db8a..91f92ca3 100644 --- a/src/engine/builtins/string.rs +++ b/src/engine/builtins/string.rs @@ -17,7 +17,7 @@ use crate::engine::object::{ObjectRef, SymbolRef}; #[cfg(test)] use crate::engine::object::{PropertyKey, WellKnownSymbol}; use crate::engine::value::{ - CreateHtmlStringBuffer, JsString, JsStringBuilder, JsStringError, Value, + CreateHtmlStringBuffer, JsString, JsStringBuilder, JsStringError, JsValue, Value, }; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -406,7 +406,9 @@ impl Runtime { ) -> Result<(), RuntimeError> { let canonical_key = self.intern_property_key(canonical)?; let value = match self.get_property_in_realm(realm, string_prototype, &canonical_key)? { - Completion::Return(value @ Value::Object(_)) => value, + Completion::Return(value @ JsValue::Object(_)) => { + self.root_and_release_jsvalue(value)? + } Completion::Return(_) => { return Err(RuntimeError::Invariant( "String canonical alias target was not callable", @@ -490,11 +492,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "String static did not receive a generic invocation", )); }; + invocation.release(self)?; match selector { StringStaticKind::FromCharCode => self.call_string_from_char_code(realm, arguments), StringStaticKind::FromCodePoint => self.call_string_from_code_point(realm, arguments), @@ -517,11 +521,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "String codePointRange did not receive a generic invocation", )); }; + invocation.release(self)?; factory::finish( self, realm, @@ -604,17 +610,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - search::finish( - self, - realm, - search::StringSearchStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + search::finish( self, realm, - search::StringSearchKind::Index(selector), - &invocation, - arguments, - )?, - ) + search::StringSearchStep::start( + self, + realm, + search::StringSearchKind::Index(selector), + invocation, + arguments, + )?, + ) + }) } fn finish_string_index_of( &self, @@ -655,7 +663,7 @@ impl Runtime { } }; - Ok(Completion::Return(Value::Int(result))) + Ok(Completion::Return(self.into_jsvalue(Value::Int(result))?)) } /// Internal-class fallback of pinned QuickJS `js_is_regexp` after an @@ -729,17 +737,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - search::finish( - self, - realm, - search::StringSearchStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + search::finish( self, realm, - search::StringSearchKind::Includes(selector), - &invocation, - arguments, - )?, - ) + search::StringSearchStep::start( + self, + realm, + search::StringSearchKind::Includes(selector), + invocation, + arguments, + )?, + ) + }) } fn finish_string_includes( &self, @@ -776,7 +786,7 @@ impl Runtime { start >= 0 && string_region_matches(&source, &needle, start) } }; - Ok(Completion::Return(Value::Bool(found))) + Ok(Completion::Return(self.into_jsvalue(Value::Bool(found))?)) } /// Rust port of pinned QuickJS `js_string_split` for the generic @@ -790,35 +800,41 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - split::finish( - self, - realm, - split::StringSplitStep::start(self, realm, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + split::finish( + self, + realm, + split::StringSplitStep::start(self, realm, invocation, arguments)?, + ) + }) } fn finish_string_split( &self, realm: ContextId, source: JsString, result: ObjectRef, - separator: &Value, + separator: &crate::engine::value::JsValue, separator_string: JsString, limit: u32, ) -> Result { let mut length = 0_u32; if limit == 0 { - return Ok(Completion::Return(Value::Object(result))); + return Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )); } - if matches!(separator, Value::Undefined) { + if matches!(separator, crate::engine::value::JsValue::Undefined) { if let Some(value) = self.define_string_split_element( realm, &result, &mut length, Value::String(source.clone()), )? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } - return Ok(Completion::Return(Value::Object(result))); + return Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )); } let source_len = source.len(); @@ -831,10 +847,12 @@ impl Runtime { &mut length, Value::String(source), )? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } } - return Ok(Completion::Return(Value::Object(result))); + return Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )); } if separator_len == 0 { @@ -845,13 +863,15 @@ impl Runtime { &mut length, Value::String(source.sub_string(index, index + 1)), )? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } if length == limit { break; } } - return Ok(Completion::Return(Value::Object(result))); + return Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )); } let source_len_i32 = i32::try_from(source_len).map_err(|_| { @@ -876,10 +896,12 @@ impl Runtime { usize::try_from(end).expect("non-negative split end fits usize"), )), )? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } if length == limit { - return Ok(Completion::Return(Value::Object(result))); + return Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )); } start = end + separator_len_i32; } @@ -892,9 +914,11 @@ impl Runtime { source_len, )), )? { - return Ok(Completion::Throw(value)); + return Ok(Completion::Throw(self.into_jsvalue(value)?)); } - Ok(Completion::Return(Value::Object(result))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )) } /// CreateDataProperty on the fresh result Array. `JsString::MAX_LEN` keeps @@ -928,17 +952,19 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - search::finish( - self, - realm, - search::StringSearchStep::start( + self.dispatch_borrowed_invocation(invocation, |invocation| { + search::finish( self, realm, - search::StringSearchKind::Subrange(selector), - &invocation, - arguments, - )?, - ) + search::StringSearchStep::start( + self, + realm, + search::StringSearchKind::Subrange(selector), + invocation, + arguments, + )?, + ) + }) } fn finish_string_subrange( &self, @@ -988,9 +1014,9 @@ impl Runtime { .map_err(|_| RuntimeError::Invariant("String subrange start became negative"))?; let range_end = usize::try_from(range_end) .map_err(|_| RuntimeError::Invariant("String subrange end became negative"))?; - Ok(Completion::Return(Value::String( + Ok(Completion::Return(self.into_jsvalue(Value::String( source.sub_string(range_start, range_end), - ))) + ))?)) } /// Rust port of pinned QuickJS `js_string_repeat`, including its distinct @@ -1018,18 +1044,20 @@ impl Runtime { arguments: &NativeArguments, string_limit: usize, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::Repeat, - &invocation, - Some(arguments), - string_limit, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::Repeat, + invocation, + Some(arguments), + string_limit, + )?, + ) + }) } fn finish_string_repeat( &self, @@ -1039,7 +1067,7 @@ impl Runtime { string_limit: usize, ) -> Result { if !(0..=2_147_483_647).contains(&count) { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid repeat count", @@ -1050,21 +1078,23 @@ impl Runtime { let repeated = match source.repeat_with_limit(count, string_limit) { Ok(value) => value, Err(JsStringError::TooLong) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid string length", )?)); } Err(JsStringError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?)); } }; - Ok(Completion::Return(Value::String(repeated))) + Ok(Completion::Return( + self.into_jsvalue(Value::String(repeated))?, + )) } /// Rust port of pinned QuickJS `js_string_pad`. The typed selector mirrors @@ -1094,18 +1124,20 @@ impl Runtime { arguments: &NativeArguments, string_limit: usize, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::Pad(selector), - &invocation, - Some(arguments), - string_limit, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::Pad(selector), + invocation, + Some(arguments), + string_limit, + )?, + ) + }) } fn finish_string_pad( &self, @@ -1117,7 +1149,9 @@ impl Runtime { string_limit: usize, ) -> Result { if filler.as_ref().is_some_and(JsString::is_empty) { - return Ok(Completion::Return(Value::String(source))); + return Ok(Completion::Return( + self.into_jsvalue(Value::String(source))?, + )); } let target = usize::try_from(target) @@ -1130,21 +1164,23 @@ impl Runtime { ) { Ok(value) => value, Err(JsStringError::TooLong) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Range, "invalid string length", )?)); } Err(JsStringError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?)); } }; - Ok(Completion::Return(Value::String(padded))) + Ok(Completion::Return( + self.into_jsvalue(Value::String(padded))?, + )) } /// Rust port of pinned QuickJS `js_string_trim`. The selector retains its @@ -1156,18 +1192,20 @@ impl Runtime { selector: StringTrimKind, invocation: NativeInvocation, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::Trim(selector), - &invocation, - None, - JsString::MAX_LEN, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::Trim(selector), + invocation, + None, + JsString::MAX_LEN, + )?, + ) + }) } fn finish_string_trim( &self, @@ -1183,7 +1221,7 @@ impl Runtime { let trimmed = match source.trim_whitespace(trim_start, trim_end) { Ok(value) => value, Err(JsStringError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", @@ -1195,7 +1233,9 @@ impl Runtime { )); } }; - Ok(Completion::Return(Value::String(trimmed))) + Ok(Completion::Return( + self.into_jsvalue(Value::String(trimmed))?, + )) } /// Rust port of pinned QuickJS `js_string_toLowerCase`. Its magic bit @@ -1217,18 +1257,20 @@ impl Runtime { invocation: NativeInvocation, string_limit: usize, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::Case(selector), - &invocation, - None, - string_limit, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::Case(selector), + invocation, + None, + string_limit, + )?, + ) + }) } fn finish_string_case( &self, @@ -1248,14 +1290,16 @@ impl Runtime { JsStringError::TooLong => "string too long", JsStringError::OutOfMemory => "out of memory", }; - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, message, )?)); } }; - Ok(Completion::Return(Value::String(converted))) + Ok(Completion::Return( + self.into_jsvalue(Value::String(converted))?, + )) } /// Rust port of pinned QuickJS `js_string_normalize`. Receiver coercion @@ -1282,18 +1326,20 @@ impl Runtime { arguments: &NativeArguments, string_limit: usize, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::Normalize, - &invocation, - Some(arguments), - string_limit, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::Normalize, + invocation, + Some(arguments), + string_limit, + )?, + ) + }) } fn finish_string_normalize( &self, @@ -1313,14 +1359,16 @@ impl Runtime { JsStringError::TooLong => "string too long", JsStringError::OutOfMemory => "out of memory", }; - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, message, )?)); } }; - Ok(Completion::Return(Value::String(normalized))) + Ok(Completion::Return( + self.into_jsvalue(Value::String(normalized))?, + )) } /// Rust port of pinned QuickJS `js_string_localeCompare`. QuickJS's @@ -1334,18 +1382,20 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::LocaleCompare, - &invocation, - Some(arguments), - JsString::MAX_LEN, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::LocaleCompare, + invocation, + Some(arguments), + JsString::MAX_LEN, + )?, + ) + }) } fn finish_string_locale_compare( &self, @@ -1359,7 +1409,7 @@ impl Runtime { ) { Ok(value) => value, Err(JsStringError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", @@ -1377,7 +1427,7 @@ impl Runtime { ) { Ok(value) => value, Err(JsStringError::OutOfMemory) => { - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", @@ -1399,7 +1449,9 @@ impl Runtime { std::cmp::Ordering::Equal => 0, std::cmp::Ordering::Greater => 1, }); - Ok(Completion::Return(Value::Int(comparison))) + Ok(Completion::Return( + self.into_jsvalue(Value::Int(comparison))?, + )) } /// Rust port of pinned QuickJS `js_string_CreateHTML`. Receiver coercion @@ -1430,18 +1482,20 @@ impl Runtime { arguments: &NativeArguments, string_limit: usize, ) -> Result { - text::finish( - self, - realm, - text::StringTextStep::start_with_limit( + self.dispatch_borrowed_invocation(invocation, |invocation| { + text::finish( self, realm, - text::StringTextKind::Html(selector), - &invocation, - Some(arguments), - string_limit, - )?, - ) + text::StringTextStep::start_with_limit( + self, + realm, + text::StringTextKind::Html(selector), + invocation, + Some(arguments), + string_limit, + )?, + ) + }) } fn finish_string_create_html( &self, @@ -1457,13 +1511,15 @@ impl Runtime { JsStringError::TooLong => "string too long", JsStringError::OutOfMemory => "out of memory", }; - return Ok(Completion::Throw(self.new_native_error( + return Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Internal, message, )?)); } }; - Ok(Completion::Return(Value::String(result))) + Ok(Completion::Return( + self.into_jsvalue(Value::String(result))?, + )) } } diff --git a/src/engine/builtins/string/factory.rs b/src/engine/builtins/string/factory.rs index b2629b6a..b0c50902 100644 --- a/src/engine/builtins/string/factory.rs +++ b/src/engine/builtins/string/factory.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::StringStaticKind, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{JsString, JsStringBuilder, Value, conversion::NativeConversion}, + value::{JsString, JsStringBuilder, JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::NativeArguments}, }; #[derive(Clone, Copy)] @@ -28,11 +28,11 @@ impl StringFactoryKind { pub(crate) enum StringFactoryStep { Complete(Completion), Number { - value: Value, + value: JsValue, resume: StringFactoryResume, }, String { - value: Value, + value: JsValue, resume: StringFactoryResume, }, Read { @@ -96,10 +96,14 @@ impl StringFactoryStep { arguments: &NativeArguments, limit: usize, ) -> Result { + let mut readable = Vec::with_capacity(arguments.readable.len()); + for value in &arguments.readable { + readable.push(runtime.root_value(value)?); + } let mut resume = StringFactoryResume(Box::new(StringFactoryResumeState { realm, kind, - arguments: arguments.readable.clone(), + arguments: readable, actual: arguments.actual_arg_count, cooked: None, raw: None, @@ -119,7 +123,9 @@ impl StringFactoryStep { let cooked = match runtime.native_to_object(realm, template.clone())? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; resume.cooked = Some(cooked.clone()); @@ -145,23 +151,26 @@ impl StringFactoryStep { .ok_or(RuntimeError::Invariant( "String codePointRange start argv was not padded", ))?; - Ok(Self::Number { value, resume }) + Ok(Self::Number { + value: runtime.into_jsvalue(value)?, + resume, + }) } } } } impl StringFactoryResume { - fn abrupt(self, value: Value) -> StringFactoryStep { + fn abrupt(self, value: JsValue) -> StringFactoryStep { StringFactoryStep::Complete(Completion::Throw(value)) } - fn complete(mut self) -> Result { + fn complete(mut self, runtime: &Runtime) -> Result { let builder = self .0 .builder .take() .ok_or(RuntimeError::Invariant("String factory lost builder"))?; Ok(StringFactoryStep::Complete(Completion::Return( - Value::String(builder.finish()?), + runtime.into_jsvalue(Value::String(builder.finish()?))?, ))) } fn builder(&mut self) -> Result<&mut JsStringBuilder, RuntimeError> { @@ -177,7 +186,7 @@ impl StringFactoryResume { ) { self.0.chunk = Value::Undefined; if self.0.index == self.0.length { - return self.complete(); + return self.complete(runtime); } self.0.phase = Phase::Chunk; return Ok(StringFactoryStep::Read { @@ -204,7 +213,7 @@ impl StringFactoryResume { ) { if let Value::Int(value) = value { if !(0..=0x10_ffff).contains(&value) { - let error = runtime.new_native_error( + let error = runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid code point", @@ -217,11 +226,11 @@ impl StringFactoryResume { } } return Ok(StringFactoryStep::Number { - value, + value: runtime.into_jsvalue(value)?, resume: self, }); } - self.complete() + self.complete(runtime) } pub(crate) fn number( mut self, @@ -230,7 +239,9 @@ impl StringFactoryResume { ) -> Result { let number = match result { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); + } }; match self.0.phase { Phase::Characters => { @@ -245,7 +256,7 @@ impl StringFactoryResume { || number > 0x10_ffff as f64 || number.fract() != 0.0 { - let error = runtime.new_native_error( + let error = runtime.new_native_error_jsvalue( self.0.realm, NativeErrorKind::Range, "invalid code point", @@ -262,7 +273,9 @@ impl StringFactoryResume { self.0.length = match runtime.native_to_length(self.0.realm, &Value::number(number))? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); + } }; self.0.builder = Some(JsStringBuilder::with_limit(0, self.0.limit)); self.next(runtime) @@ -279,7 +292,7 @@ impl StringFactoryResume { "String codePointRange end argv was not padded", ))?; Ok(StringFactoryStep::Number { - value, + value: runtime.into_jsvalue(value)?, resume: self, }) } @@ -296,7 +309,7 @@ impl StringFactoryResume { builder.push_code_point(point)?; } Ok(StringFactoryStep::Complete(Completion::Return( - Value::String(builder.finish()?), + runtime.into_jsvalue(Value::String(builder.finish()?))?, ))) } _ => Err(RuntimeError::Invariant( @@ -310,14 +323,16 @@ impl StringFactoryResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => return Ok(self.abrupt(value)), }; match self.0.phase { Phase::Raw => { let raw = match runtime.native_to_object(self.0.realm, value)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); + } }; self.0.raw = Some(raw.clone()); self.0.phase = Phase::Length; @@ -331,14 +346,14 @@ impl StringFactoryResume { Phase::Length => { self.0.length_value = value.clone(); Ok(StringFactoryStep::Number { - value, + value: runtime.into_jsvalue(value)?, resume: self, }) } Phase::Chunk => { self.0.chunk = value.clone(); Ok(StringFactoryStep::String { - value, + value: runtime.into_jsvalue(value)?, resume: self, }) } @@ -354,7 +369,9 @@ impl StringFactoryResume { ) -> Result { let value = match result { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(self.abrupt(value)), + NativeConversion::Throw(value) => { + return Ok(self.abrupt(runtime.into_jsvalue(value)?)); + } }; match self.0.phase { Phase::Chunk => { @@ -381,7 +398,7 @@ impl StringFactoryResume { "String.raw substitution argv was not readable", ))?; Ok(StringFactoryStep::String { - value, + value: runtime.into_jsvalue(value)?, resume: self, }) } @@ -404,12 +421,14 @@ pub(crate) fn finish( loop { step = match step { StringFactoryStep::Complete(result) => return Ok(result), - StringFactoryStep::Number { value, resume } => { - resume.number(runtime, runtime.native_to_number(realm, &value)?)? - } - StringFactoryStep::String { value, resume } => { - resume.string(runtime, runtime.native_to_js_string(realm, &value)?)? - } + StringFactoryStep::Number { value, resume } => resume.number( + runtime, + runtime.native_to_number(realm, &runtime.root_and_release_jsvalue(value)?)?, + )?, + StringFactoryStep::String { value, resume } => resume.string( + runtime, + runtime.native_to_js_string(realm, &runtime.root_and_release_jsvalue(value)?)?, + )?, StringFactoryStep::Read { object, key, diff --git a/src/engine/builtins/string/regexp.rs b/src/engine/builtins/string/regexp.rs index e67a380e..99670968 100644 --- a/src/engine/builtins/string/regexp.rs +++ b/src/engine/builtins/string/regexp.rs @@ -5,7 +5,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{ConstructorRef, DirectCallTarget, NativeArguments, NativeInvocation}, @@ -89,11 +89,13 @@ impl Runtime { arguments: &NativeArguments, protocol: StringProtocolKind, ) -> Result { - finish( - self, - realm, - StringProtocolStep::start(self, realm, protocol, &invocation, arguments)?, - ) + self.dispatch_borrowed_invocation(invocation, |invocation| { + finish( + self, + realm, + StringProtocolStep::start(self, realm, protocol, invocation, arguments)?, + ) + }) } } @@ -156,43 +158,48 @@ impl StringProtocolStep { let NativeInvocation::Call { this_value } = invocation else { return Err(RuntimeError::Invariant(kind.invocation_invariant())); }; + let this_value = runtime.root_value(this_value)?; if matches!(this_value, Value::Null | Value::Undefined) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot convert to object", )?, ))); } - let pattern = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant(kind.argument_invariant()))? - .clone(); + let pattern = runtime.root_value( + arguments + .readable + .first() + .ok_or(RuntimeError::Invariant(kind.argument_invariant()))?, + )?; let key = PropertyKey::from(runtime.well_known_symbol(kind.symbol())); let resume = StringProtocolResume(Box::new(StringProtocolResumeState { step_pending: StringProtocolStepPending::default(), realm, kind, - receiver: this_value.clone(), + receiver: this_value, pattern, phase: ProtocolPhase::Method, })); if let Value::Object(object) = &resume.pattern { Ok(Self::make_read(object.clone(), key, resume)) } else { - Ok(resume.source()) + resume.source(runtime) } } } impl StringProtocolResume { - fn source(mut self) -> StringProtocolStep { - StringProtocolStep::make_primitive(self.0.receiver.clone(), { - let updated_0 = ProtocolPhase::Source; - self.0.phase = updated_0; - self - }) + fn source(mut self, runtime: &Runtime) -> Result { + Ok(StringProtocolStep::make_primitive( + runtime.unroot_value(&self.0.receiver)?, + { + let updated_0 = ProtocolPhase::Source; + self.0.phase = updated_0; + self + }, + )) } fn selected( self, @@ -200,7 +207,7 @@ impl StringProtocolResume { method: Value, ) -> Result { if matches!(method, Value::Undefined | Value::Null) { - return Ok(self.source()); + return self.source(runtime); } let receiver = self.0.pattern.clone(); let argument = self.0.receiver.clone(); @@ -219,17 +226,21 @@ impl StringProtocolResume { }; let Some(callable) = callable else { return Ok(StringProtocolStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + self.0.realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; let mut arguments = Vec::new(); if arguments.try_reserve_exact(1).is_err() { return protocol_oom(runtime, self.0.realm); } - arguments.push(argument); + arguments.push(runtime.unroot_value(&argument)?); Ok(StringProtocolStep::make_call( DirectCallTarget::Callable(callable), - receiver, + runtime.unroot_value(&receiver)?, arguments, { let updated_0 = ProtocolPhase::Called; @@ -244,7 +255,7 @@ impl StringProtocolResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(StringProtocolStep::Complete(Completion::Throw(value))); } @@ -301,29 +312,34 @@ impl StringProtocolResume { ProtocolPhase::Flags(method) => { if matches!(value, Value::Undefined | Value::Null) { return Ok(StringProtocolStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot convert to object", )?, ))); } - Ok(StringProtocolStep::make_primitive(value, { - let updated_0 = ProtocolPhase::FlagsString(method); - self.0.phase = updated_0; - self - })) + Ok(StringProtocolStep::make_primitive( + runtime.into_jsvalue(value)?, + { + let updated_0 = ProtocolPhase::FlagsString(method); + self.0.phase = updated_0; + self + }, + )) } ProtocolPhase::FlagsString(method) => { let flags = match protocol_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringProtocolStep::Complete(Completion::Throw(value))); + return Ok(StringProtocolStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !flags.utf16_units().any(|unit| unit == u16::from(b'g')) { return Ok(StringProtocolStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "regexp must have the 'g' flag", @@ -341,7 +357,9 @@ impl StringProtocolResume { let source = match protocol_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringProtocolStep::Complete(Completion::Throw(value))); + return Ok(StringProtocolStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let constructor = ObjectRef::from_borrowed_handle( @@ -361,9 +379,10 @@ impl StringProtocolResume { { return protocol_oom(runtime, realm); } - arguments.push(self.0.pattern.clone()); + arguments.push(runtime.unroot_value(&self.0.pattern)?); if all { - arguments.push(Value::String(JsString::from_static("g"))); + arguments + .push(runtime.into_jsvalue(Value::String(JsString::from_static("g")))?); } Ok(StringProtocolStep::make_construct( ConstructorRef::from_validated_object(constructor), @@ -397,7 +416,9 @@ impl StringProtocolResume { self } .call(runtime, Value::Object(regexp), value, Value::String(source)), - ProtocolPhase::Called => Ok(StringProtocolStep::Complete(Completion::Return(value))), + ProtocolPhase::Called => Ok(StringProtocolStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), } } } @@ -415,7 +436,7 @@ fn protocol_string( } fn protocol_oom(runtime: &Runtime, realm: ContextId) -> Result { Ok(StringProtocolStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Internal, "out of memory")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Internal, "out of memory")?, ))) } fn finish( @@ -437,8 +458,8 @@ fn finish( StringProtocolStep::Primitive { mut resume } => { let value = resume.take_primitive_value(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, ToPrimitiveHint::String)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? } else { Completion::Return(value) }; @@ -447,8 +468,12 @@ fn finish( } StringProtocolStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -463,7 +488,11 @@ fn finish( } StringProtocolStep::Construct { mut resume } => { let constructor = resume.take_construct_constructor(); - let arguments = resume.take_construct_arguments(); + let arguments = resume + .take_construct_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.construct_constructor_internal( @@ -482,10 +511,10 @@ fn finish( pub(crate) struct StringProtocolStepPending { object: Option, key: Option, - value: Option, + value: Option, target: Option, - receiver: Option, - arguments: Option>, + receiver: Option, + arguments: Option>, constructor: Option, } impl StringProtocolStep { @@ -498,14 +527,14 @@ impl StringProtocolStep { resume.0.step_pending.key = Some(key); Self::Read { resume } } - pub(crate) fn make_primitive(value: Value, mut resume: StringProtocolResume) -> Self { + pub(crate) fn make_primitive(value: JsValue, mut resume: StringProtocolResume) -> Self { resume.0.step_pending.value = Some(value); Self::Primitive { resume } } pub(crate) fn make_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: StringProtocolResume, ) -> Self { resume.0.step_pending.target = Some(target); @@ -515,7 +544,7 @@ impl StringProtocolStep { } pub(crate) fn make_construct( constructor: ConstructorRef, - arguments: Vec, + arguments: Vec, mut resume: StringProtocolResume, ) -> Self { resume.0.step_pending.constructor = Some(constructor); @@ -539,7 +568,7 @@ impl StringProtocolResume { .expect("StringProtocolStep::Read lost key") } - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -554,14 +583,14 @@ impl StringProtocolResume { .take() .expect("StringProtocolStep::Call lost target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .step_pending .receiver .take() .expect("StringProtocolStep::Call lost receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .step_pending .arguments @@ -576,7 +605,7 @@ impl StringProtocolResume { .take() .expect("StringProtocolStep::Construct lost constructor") } - pub(crate) fn take_construct_arguments(&mut self) -> Vec { + pub(crate) fn take_construct_arguments(&mut self) -> Vec { self.0 .step_pending .arguments diff --git a/src/engine/builtins/string/replace.rs b/src/engine/builtins/string/replace.rs index 4a86f2df..e4b1fcfd 100644 --- a/src/engine/builtins/string/replace.rs +++ b/src/engine/builtins/string/replace.rs @@ -10,7 +10,7 @@ use crate::engine::{ builtins::native::StringReplaceKind, heap::ContextId, object::{PropertyKey, WellKnownSymbol}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -37,6 +37,7 @@ impl std::ops::DerefMut for StringReplaceResume { } const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct StringReplaceResumeState { + runtime: Runtime, step_pending: StringReplaceStepPending, realm: ContextId, selector: StringReplaceKind, @@ -61,6 +62,24 @@ enum Phase { Callback { position: usize }, CallbackString { position: usize }, } +impl Drop for StringReplaceResumeState { + /// Release the internal edges the pending effect still owns when the + /// request is abandoned. Consumption goes through `Option::take`, so a + /// drained field is `None` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + if let Some(value) = self.step_pending.value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.step_pending.receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.step_pending.arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} struct ReplaceLoop { output: ReplacementStringBuffer, source: JsString, @@ -99,34 +118,28 @@ impl StringReplaceStep { "String replace family did not receive a generic-magic invocation", )); }; + let this_value = runtime.root_value(this_value)?; if matches!(this_value, Value::Undefined | Value::Null) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot convert to object", )?, ))); } - let search_value = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "String replace search argv was not padded", - ))? - .clone(); - let replace_value = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "String replace replacement argv was not padded", - ))? - .clone(); + let search_value = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("String replace search argv was not padded"), + )?)?; + let replace_value = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("String replace replacement argv was not padded"), + )?)?; let mut resume = StringReplaceResumeState { + runtime: runtime.clone(), step_pending: StringReplaceStepPending::default(), realm, selector, - receiver: this_value.clone(), + receiver: this_value, search_value, replace_value, phase: Phase::Method, @@ -154,7 +167,7 @@ impl StringReplaceStep { crate::engine::api::profiling::record_owned_execution_event( "stringreplace_resident_allocated", ); - StringReplaceResume(Box::new(resume)).publish(action) + StringReplaceResume(Box::new(resume)).publish(runtime, action) } } impl StringReplaceResume { @@ -171,20 +184,33 @@ impl StringReplaceResume { ) -> Result { let action = self.0.advance(runtime, completion)?; let action = self.0.advance_local(runtime, action)?; - self.publish(action) + self.publish(runtime, action) } - fn publish(self, action: StringReplaceAction) -> Result { + fn publish( + self, + runtime: &Runtime, + action: StringReplaceAction, + ) -> Result { Ok(match action { StringReplaceAction::Complete(result) => StringReplaceStep::Complete(result), StringReplaceAction::PreparedRead { read, key } => { StringReplaceStep::make_preparedread(read, key, self) } - StringReplaceAction::Primitive(value) => StringReplaceStep::make_primitive(value, self), + StringReplaceAction::Primitive(value) => { + StringReplaceStep::make_primitive(runtime.into_jsvalue(value)?, self) + } StringReplaceAction::Call { target, receiver, arguments, - } => StringReplaceStep::make_call(target, receiver, arguments, self), + } => { + let receiver = runtime.into_jsvalue(receiver)?; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; + StringReplaceStep::make_call(target, receiver, arguments, self) + } StringReplaceAction::Read(_) => { return Err(RuntimeError::Invariant( "local replacement read was not selected", @@ -237,7 +263,7 @@ impl StringReplaceResumeState { ); self.advance( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), )? } read => { @@ -250,7 +276,7 @@ impl StringReplaceResumeState { crate::engine::api::profiling::record_owned_execution_event( "stringreplace_primitive_local", ); - self.advance(runtime, Completion::Return(value))? + self.advance(runtime, Completion::Return(runtime.into_jsvalue(value)?))? } action @ (StringReplaceAction::Primitive(_) | StringReplaceAction::Call { .. } @@ -264,7 +290,7 @@ impl StringReplaceResumeState { completion: Completion, ) -> Result { let value = match completion { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(StringReplaceAction::Complete(Completion::Throw(value))); } @@ -289,7 +315,7 @@ impl StringReplaceResumeState { Phase::Flags => { if matches!(value, Value::Undefined | Value::Null) { return Ok(StringReplaceAction::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot convert to object", @@ -303,12 +329,14 @@ impl StringReplaceResumeState { let flags = match primitive_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringReplaceAction::Complete(Completion::Throw(value))); + return Ok(StringReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; if !flags.utf16_units().any(|unit| unit == u16::from(b'g')) { return Ok(StringReplaceAction::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "regexp must have the 'g' flag", @@ -327,13 +355,17 @@ impl StringReplaceResumeState { }; let Some(callable) = callable else { return Ok(StringReplaceAction::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; let mut arguments = Vec::new(); if arguments.try_reserve_exact(2).is_err() { return Ok(StringReplaceAction::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", @@ -349,12 +381,16 @@ impl StringReplaceResumeState { arguments, }) } - Phase::ProtocolResult => Ok(StringReplaceAction::Complete(Completion::Return(value))), + Phase::ProtocolResult => Ok(StringReplaceAction::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), Phase::Source => { self.source = Some(match primitive_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringReplaceAction::Complete(Completion::Throw(value))); + return Ok(StringReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }); self.phase = Phase::Search; @@ -364,7 +400,9 @@ impl StringReplaceResumeState { let search = match primitive_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringReplaceAction::Complete(Completion::Throw(value))); + return Ok(StringReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let functional = match &self.replace_value { @@ -401,7 +439,9 @@ impl StringReplaceResumeState { let replacement = match primitive_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringReplaceAction::Complete(Completion::Throw(value))); + return Ok(StringReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; self.cursor @@ -418,7 +458,9 @@ impl StringReplaceResumeState { let result = match primitive_string(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringReplaceAction::Complete(Completion::Throw(value))); + return Ok(StringReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let state = self @@ -479,7 +521,7 @@ impl StringReplaceResumeState { .take() .ok_or(RuntimeError::Invariant("replacement cursor disappeared"))?; return Ok(StringReplaceAction::Complete(Completion::Return( - Value::String(state.source), + runtime.into_jsvalue(Value::String(state.source))?, ))); } return self.finish_buffer(runtime); @@ -494,7 +536,7 @@ impl StringReplaceResumeState { let mut arguments = Vec::new(); if arguments.try_reserve_exact(3).is_err() { return Ok(StringReplaceAction::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( self.realm, NativeErrorKind::Internal, "out of memory", @@ -537,12 +579,16 @@ impl StringReplaceResumeState { NativeConversion::Value(_) => Err(RuntimeError::Invariant( "failed replacement buffer unexpectedly completed", )), - NativeConversion::Throw(value) => { - Ok(StringReplaceAction::Complete(Completion::Throw(value))) - } + NativeConversion::Throw(value) => Ok(StringReplaceAction::Complete( + Completion::Throw(runtime.into_jsvalue(value)?), + )), }; } - Err(value) => return Ok(StringReplaceAction::Complete(Completion::Throw(value))), + Err(value) => { + return Ok(StringReplaceAction::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); + } } state.end = position + state.search.len(); state.first = false; @@ -561,8 +607,10 @@ impl StringReplaceResumeState { .append_range(&state.source, state.end, state.source.len()); Ok(StringReplaceAction::Complete( match runtime.finish_replacement_buffer(self.realm, state.output)? { - NativeConversion::Value(value) => Completion::Return(Value::String(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => { + Completion::Return(runtime.into_jsvalue(Value::String(value))?) + } + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), }, )) } @@ -587,52 +635,61 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let mut step = StringReplaceStep::start(self, realm, selector, &invocation, arguments)?; - loop { - step = match step { - StringReplaceStep::Complete(result) => return Ok(result), - StringReplaceStep::PreparedRead { mut resume } => { - let read = resume.take_preparedread_read(); - let key = resume.take_preparedread_key(); - { - let result = match self.finish_prepared_read(realm, &key, read)? { - NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) - } - NativeConversion::Throw(value) => Completion::Throw(value), - }; - resume.resume(self, result)? + self.dispatch_borrowed_invocation(invocation, |invocation| { + let mut step = StringReplaceStep::start(self, realm, selector, invocation, arguments)?; + loop { + step = match step { + StringReplaceStep::Complete(result) => return Ok(result), + StringReplaceStep::PreparedRead { mut resume } => { + let read = resume.take_preparedread_read(); + let key = resume.take_preparedread_key(); + { + let result = match self.finish_prepared_read(realm, &key, read)? { + NativeConversion::Value(value) => Completion::Return( + self.into_jsvalue(value.unwrap_or(Value::Undefined))?, + ), + NativeConversion::Throw(value) => { + Completion::Throw(self.into_jsvalue(value)?) + } + }; + resume.resume(self, result)? + } } - } - StringReplaceStep::Primitive { mut resume } => { - let value = resume.take_primitive_value(); - { - let result = if matches!(value, Value::Object(_)) { - self.to_primitive(realm, value, ToPrimitiveHint::String)? - } else { - Completion::Return(value) - }; - resume.resume(self, result)? + StringReplaceStep::Primitive { mut resume } => { + let value = resume.take_primitive_value(); + { + let result = if matches!(value, JsValue::Object(_)) { + self.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? + } else { + Completion::Return(value) + }; + resume.resume(self, result)? + } } - } - StringReplaceStep::Call { mut resume } => { - let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); - { - let DirectCallTarget::Callable(callable) = target else { - return Err(RuntimeError::Invariant( - "String replacement requested an invalid call target", - )); - }; - resume.resume( - self, - self.call_internal(realm, &callable, receiver, &arguments)?, - )? + StringReplaceStep::Call { mut resume } => { + let target = resume.take_call_target(); + let receiver = + self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; + { + let DirectCallTarget::Callable(callable) = target else { + return Err(RuntimeError::Invariant( + "String replacement requested an invalid call target", + )); + }; + resume.resume( + self, + self.call_internal(realm, &callable, receiver, &arguments)?, + )? + } } - } - }; - } + }; + } + }) } } @@ -645,13 +702,19 @@ mod tests { let runtime = Runtime::new(); let context = runtime.new_context(); let invocation = NativeInvocation::Call { - this_value: Value::String(JsString::from_static("aba")), + this_value: runtime + .into_jsvalue(Value::String(JsString::from_static("aba"))) + .unwrap(), }; let arguments = NativeArguments { actual_arg_count: 2, readable: vec![ - Value::String(JsString::from_static("a")), - Value::String(JsString::from_static("$&x")), + runtime + .into_jsvalue(Value::String(JsString::from_static("a"))) + .unwrap(), + runtime + .into_jsvalue(Value::String(JsString::from_static("$&x"))) + .unwrap(), ], }; let profile = crate::engine::api::profiling::CostProfile::start(); @@ -665,7 +728,10 @@ mod tests { .unwrap() else { panic!("primitive replace must complete locally") }; - assert_eq!(value, Value::String(JsString::from_static("axbax"))); + assert_eq!( + runtime.root_value(&value).unwrap(), + Value::String(JsString::from_static("axbax")) + ); assert_eq!( profile .snapshot() @@ -690,11 +756,16 @@ mod tests { }; let callback_id = function.object_id(); let invocation = NativeInvocation::Call { - this_value: Value::Object(receiver), + this_value: runtime.into_jsvalue(Value::Object(receiver)).unwrap(), }; let arguments = NativeArguments { actual_arg_count: 2, - readable: vec![Value::String(JsString::from_static("a")), callback], + readable: vec![ + runtime + .into_jsvalue(Value::String(JsString::from_static("a"))) + .unwrap(), + runtime.into_jsvalue(callback).unwrap(), + ], }; let StringReplaceStep::Primitive { mut resume } = StringReplaceStep::start( &runtime, @@ -706,24 +777,41 @@ mod tests { .unwrap() else { panic!("expected source conversion") }; - drop(resume.take_primitive_value()); + runtime + .release_jsvalue(resume.take_primitive_value()) + .unwrap(); let address = (&*resume.0) as *const StringReplaceResumeState; - drop(invocation); - drop(arguments); + { + let NativeInvocation::Call { this_value } = invocation else { + unreachable!() + }; + runtime.release_jsvalue(this_value).unwrap(); + for value in arguments.readable { + runtime.release_jsvalue(value).unwrap(); + } + } // Source is a real Object conversion wait. Its primitive reply now // advances search conversion locally to the actual replacer callback. let StringReplaceStep::Call { mut resume } = resume .resume( &runtime, - Completion::Return(Value::String(JsString::from_static("aa"))), + Completion::Return( + runtime + .into_jsvalue(Value::String(JsString::from_static("aa"))) + .unwrap(), + ), ) .unwrap() else { panic!("expected replacer call") }; drop(resume.take_call_target()); - drop(resume.take_call_receiver()); - drop(resume.take_call_arguments()); + runtime + .release_jsvalue(resume.take_call_receiver()) + .unwrap(); + for value in resume.take_call_arguments() { + runtime.release_jsvalue(value).unwrap(); + } assert_eq!((&*resume.0) as *const StringReplaceResumeState, address); runtime.run_gc().unwrap(); for id in [receiver_id, callback_id] { @@ -775,10 +863,10 @@ mod local_replace_tests { pub(crate) struct StringReplaceStepPending { read: Option, key: Option, - value: Option, + value: Option, target: Option, - receiver: Option, - arguments: Option>, + receiver: Option, + arguments: Option>, } impl StringReplaceStep { pub(crate) fn make_preparedread( @@ -790,14 +878,14 @@ impl StringReplaceStep { resume.0.step_pending.key = Some(key); Self::PreparedRead { resume } } - pub(crate) fn make_primitive(value: Value, mut resume: StringReplaceResume) -> Self { + pub(crate) fn make_primitive(value: JsValue, mut resume: StringReplaceResume) -> Self { resume.0.step_pending.value = Some(value); Self::Primitive { resume } } pub(crate) fn make_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: StringReplaceResume, ) -> Self { resume.0.step_pending.target = Some(target); @@ -822,7 +910,7 @@ impl StringReplaceResume { .expect("StringReplaceStep::PreparedRead lost key") } - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -837,14 +925,14 @@ impl StringReplaceResume { .take() .expect("StringReplaceStep::Call lost target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .step_pending .receiver .take() .expect("StringReplaceStep::Call lost receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .step_pending .arguments diff --git a/src/engine/builtins/string/search.rs b/src/engine/builtins/string/search.rs index f7dfb38e..9b9a6756 100644 --- a/src/engine/builtins/string/search.rs +++ b/src/engine/builtins/string/search.rs @@ -6,7 +6,7 @@ use crate::engine::{ builtins::native::{StringIncludesKind, StringIndexOfKind, StringSubrangeKind}, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -36,7 +36,7 @@ pub(crate) enum StringSearchStep { resume: StringSearchResume, }, Primitive { - value: Value, + value: JsValue, hint: ToPrimitiveHint, resume: StringSearchResume, }, @@ -83,33 +83,31 @@ impl StringSearchStep { "String search did not receive a call", )); }; + let this_value = runtime.root_value(this_value)?; if matches!(this_value, Value::Undefined | Value::Null) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "null or undefined are forbidden", )?, ))); } + let first = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("String search first argument was not padded"), + )?)?; + let second = match arguments.readable.get(1) { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + }; Ok(Self::Primitive { - value: this_value.clone(), + value: runtime.into_jsvalue(this_value)?, hint: ToPrimitiveHint::String, resume: StringSearchResume(Box::new(StringSearchResumeState { realm, kind, - first: arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "String search first argument was not padded", - ))? - .clone(), - second: arguments - .readable - .get(1) - .cloned() - .unwrap_or(Value::Undefined), + first, + second, actual: arguments.actual_arg_count, phase: SearchPhase::Source, })), @@ -119,7 +117,7 @@ impl StringSearchStep { impl StringSearchResume { fn primitive( mut self, - value: Value, + value: JsValue, hint: ToPrimitiveHint, phase: SearchPhase, ) -> StringSearchStep { @@ -133,9 +131,9 @@ impl StringSearchResume { }, } } - fn needle(self, source: JsString) -> StringSearchStep { - let value = self.0.first.clone(); - self.primitive(value, ToPrimitiveHint::String, SearchPhase::Needle(source)) + fn needle(self, runtime: &Runtime, source: JsString) -> Result { + let value = runtime.unroot_value(&self.0.first)?; + Ok(self.primitive(value, ToPrimitiveHint::String, SearchPhase::Needle(source))) } fn finish_search( &self, @@ -164,7 +162,7 @@ impl StringSearchResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(StringSearchStep::Complete(Completion::Throw(value))); } @@ -175,7 +173,9 @@ impl StringSearchResume { let source = match string_value(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringSearchStep::Complete(Completion::Throw(value))); + return Ok(StringSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.kind { @@ -185,7 +185,7 @@ impl StringSearchResume { "String length exceeded QuickJS's signed index range", ) })?; - let value = self.0.first.clone(); + let value = runtime.unroot_value(&self.0.first)?; Ok(self.primitive( value, ToPrimitiveHint::Number, @@ -206,10 +206,10 @@ impl StringSearchResume { }, }) } else { - Ok(self.needle(source)) + self.needle(runtime, source) } } - StringSearchKind::Index(_) => Ok(self.needle(source)), + StringSearchKind::Index(_) => self.needle(runtime, source), } } SearchPhase::Regexp(source) => { @@ -219,25 +219,27 @@ impl StringSearchResume { let regexp = runtime.is_regexp_from_match(object, &value)?; if regexp { return Ok(StringSearchStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "regexp not supported", )?, ))); } - Ok({ + { let updated_0 = SearchPhase::Source; self.0.phase = updated_0; self } - .needle(source)) + .needle(runtime, source) } SearchPhase::Needle(source) => { let needle = match string_value(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringSearchStep::Complete(Completion::Throw(value))); + return Ok(StringSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; i32::try_from(source.len()).map_err(|_| { @@ -257,7 +259,7 @@ impl StringSearchResume { && !(matches!(next.kind, StringSearchKind::Includes(_)) && matches!(next.second, Value::Undefined)) { - let value = next.second.clone(); + let value = runtime.unroot_value(&next.second)?; Ok(next.primitive( value, ToPrimitiveHint::Number, @@ -271,7 +273,9 @@ impl StringSearchResume { let position = match number_value(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringSearchStep::Complete(Completion::Throw(value))); + return Ok(StringSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; { @@ -285,7 +289,9 @@ impl StringSearchResume { let start = match number_value(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringSearchStep::Complete(Completion::Throw(value))); + return Ok(StringSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let next = { @@ -301,7 +307,7 @@ impl StringSearchResume { runtime.finish_string_subrange(kind, source, start, None)?, )) } else { - let value = next.second.clone(); + let value = runtime.unroot_value(&next.second)?; Ok(next.primitive( value, ToPrimitiveHint::Number, @@ -313,7 +319,9 @@ impl StringSearchResume { let end = match number_value(runtime, realm, value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringSearchStep::Complete(Completion::Throw(value))); + return Ok(StringSearchStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let StringSearchKind::Subrange(kind) = self.0.kind else { @@ -374,8 +382,8 @@ pub(super) fn finish( hint, resume, } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/string/split.rs b/src/engine/builtins/string/split.rs index 6b4cc72e..1258b25f 100644 --- a/src/engine/builtins/string/split.rs +++ b/src/engine/builtins/string/split.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{DirectCallTarget, NativeArguments, NativeInvocation}, @@ -31,9 +31,9 @@ const _: () = assert!(std::mem::size_of::() <= 8); pub(crate) struct StringSplitResumeState { step_pending: StringSplitStepPending, realm: ContextId, - receiver: Value, - separator: Value, - limit: Value, + receiver: JsValue, + separator: JsValue, + limit: JsValue, phase: SplitPhase, } enum SplitPhase { @@ -62,66 +62,74 @@ impl StringSplitStep { "String split did not receive a generic invocation", )); }; - if matches!(this_value, Value::Undefined | Value::Null) { + let this_value = runtime.dup_jsvalue(this_value)?; + if matches!(this_value, JsValue::Undefined | JsValue::Null) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot convert to object", )?, ))); } - let separator = arguments - .readable - .first() - .ok_or(RuntimeError::Invariant( - "String split separator argv was not padded", - ))? - .clone(); - let limit = arguments - .readable - .get(1) - .ok_or(RuntimeError::Invariant( - "String split limit argv was not padded", - ))? - .clone(); + let separator = runtime.dup_jsvalue(arguments.readable.first().ok_or( + RuntimeError::Invariant("String split separator argv was not padded"), + )?)?; + let limit = runtime.dup_jsvalue(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("String split limit argv was not padded"), + )?)?; let resume = StringSplitResume(Box::new(StringSplitResumeState { step_pending: StringSplitStepPending::default(), realm, - receiver: this_value.clone(), + receiver: this_value, separator, limit, phase: SplitPhase::Method, })); - if let Value::Object(object) = &resume.separator { + if let JsValue::Object(id) = &resume.separator { + let object = ObjectRef::from_borrowed_handle(runtime.clone(), *id)?; Ok(Self::make_read( - object.clone(), + object, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Split)), resume, )) } else { - Ok(resume.source()) + resume.source(runtime) } } } impl StringSplitResume { - fn source(mut self) -> StringSplitStep { - StringSplitStep::make_primitive(self.0.receiver.clone(), ToPrimitiveHint::String, { - let updated_0 = SplitPhase::Source; - self.0.phase = updated_0; - self - }) + fn source(mut self, runtime: &Runtime) -> Result { + Ok(StringSplitStep::make_primitive( + runtime.dup_jsvalue(&self.0.receiver)?, + ToPrimitiveHint::String, + { + let updated_0 = SplitPhase::Source; + self.0.phase = updated_0; + self + }, + )) } - fn separator(mut self, source: JsString, result: ObjectRef, limit: u32) -> StringSplitStep { - StringSplitStep::make_primitive(self.0.separator.clone(), ToPrimitiveHint::String, { - let updated_0 = SplitPhase::Separator { - source, - result, - limit, - }; - self.0.phase = updated_0; - self - }) + fn separator( + mut self, + runtime: &Runtime, + source: JsString, + result: ObjectRef, + limit: u32, + ) -> Result { + Ok(StringSplitStep::make_primitive( + runtime.dup_jsvalue(&self.0.separator)?, + ToPrimitiveHint::String, + { + let updated_0 = SplitPhase::Separator { + source, + result, + limit, + }; + self.0.phase = updated_0; + self + }, + )) } pub(crate) fn resume( mut self, @@ -129,7 +137,7 @@ impl StringSplitResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(StringSplitStep::Complete(Completion::Throw(value))); } @@ -138,7 +146,7 @@ impl StringSplitResume { match self.0.phase { SplitPhase::Method => { if matches!(value, Value::Undefined | Value::Null) { - return Ok(self.source()); + return self.source(runtime); } let callable = match value { Value::Object(object) => runtime.as_callable(&object)?, @@ -146,24 +154,28 @@ impl StringSplitResume { }; let Some(callable) = callable else { return Ok(StringSplitStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + realm, + NativeErrorKind::Type, + "not a function", + )?, ))); }; let mut arguments = Vec::new(); if arguments.try_reserve_exact(2).is_err() { return Ok(StringSplitStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Internal, "out of memory", )?, ))); } - arguments.push(self.0.receiver.clone()); - arguments.push(self.0.limit.clone()); + arguments.push(runtime.dup_jsvalue(&self.0.receiver)?); + arguments.push(runtime.dup_jsvalue(&self.0.limit)?); Ok(StringSplitStep::make_call( DirectCallTarget::Callable(callable), - self.0.separator.clone(), + runtime.dup_jsvalue(&self.0.separator)?, arguments, { let updated_0 = SplitPhase::Called; @@ -172,7 +184,9 @@ impl StringSplitResume { }, )) } - SplitPhase::Called => Ok(StringSplitStep::Complete(Completion::Return(value))), + SplitPhase::Called => Ok(StringSplitStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))), SplitPhase::Source => { if matches!(value, Value::Object(_)) { return Err(RuntimeError::Invariant( @@ -182,15 +196,17 @@ impl StringSplitResume { let source = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value.linearize(), NativeConversion::Throw(value) => { - return Ok(StringSplitStep::Complete(Completion::Throw(value))); + return Ok(StringSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let result = runtime.new_array(realm)?; - if matches!(self.0.limit, Value::Undefined) { - Ok(self.separator(source, result, u32::MAX)) + if matches!(self.0.limit, JsValue::Undefined) { + self.separator(runtime, source, result, u32::MAX) } else { Ok(StringSplitStep::make_primitive( - self.0.limit.clone(), + runtime.dup_jsvalue(&self.0.limit)?, ToPrimitiveHint::Number, { let updated_0 = SplitPhase::Limit { source, result }; @@ -209,15 +225,17 @@ impl StringSplitResume { let limit = match runtime.native_to_number(realm, &value)? { NativeConversion::Value(value) => Runtime::to_uint32_number(value), NativeConversion::Throw(value) => { - return Ok(StringSplitStep::Complete(Completion::Throw(value))); + return Ok(StringSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; - Ok({ + { let updated_0 = SplitPhase::Called; self.0.phase = updated_0; self } - .separator(source, result, limit)) + .separator(runtime, source, result, limit) } SplitPhase::Separator { source, @@ -232,7 +250,9 @@ impl StringSplitResume { let separator = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value.linearize(), NativeConversion::Throw(value) => { - return Ok(StringSplitStep::Complete(Completion::Throw(value))); + return Ok(StringSplitStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; Ok(StringSplitStep::Complete(runtime.finish_string_split( @@ -267,8 +287,8 @@ pub(super) fn finish( let value = resume.take_primitive_value(); let hint = resume.take_primitive_hint(); { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; @@ -277,8 +297,12 @@ pub(super) fn finish( } StringSplitStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let DirectCallTarget::Callable(callable) = target else { return Err(RuntimeError::Invariant( @@ -299,11 +323,11 @@ pub(super) fn finish( pub(crate) struct StringSplitStepPending { object: Option, key: Option, - value: Option, + value: Option, hint: Option, target: Option, - receiver: Option, - arguments: Option>, + receiver: Option, + arguments: Option>, } impl StringSplitStep { pub(crate) fn make_read( @@ -316,7 +340,7 @@ impl StringSplitStep { Self::Read { resume } } pub(crate) fn make_primitive( - value: Value, + value: JsValue, hint: ToPrimitiveHint, mut resume: StringSplitResume, ) -> Self { @@ -326,8 +350,8 @@ impl StringSplitStep { } pub(crate) fn make_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: StringSplitResume, ) -> Self { resume.0.step_pending.target = Some(target); @@ -352,7 +376,7 @@ impl StringSplitResume { .expect("StringSplitStep::Read lost key") } - pub(crate) fn take_primitive_value(&mut self) -> Value { + pub(crate) fn take_primitive_value(&mut self) -> JsValue { self.0 .step_pending .value @@ -374,14 +398,14 @@ impl StringSplitResume { .take() .expect("StringSplitStep::Call lost target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .step_pending .receiver .take() .expect("StringSplitStep::Call lost receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .step_pending .arguments diff --git a/src/engine/builtins/string/tests.rs b/src/engine/builtins/string/tests.rs index f575dc13..33900a01 100644 --- a/src/engine/builtins/string/tests.rs +++ b/src/engine/builtins/string/tests.rs @@ -23,6 +23,26 @@ const STRING_CASE_ENTRIES: [(&str, StringCaseKind); 4] = [ ("toLocaleUpperCase", StringCaseKind::Upper), ]; +fn js(runtime: &Runtime, value: Value) -> JsValue { + runtime.into_jsvalue(value).unwrap() +} + +#[track_caller] +fn returned(runtime: &Runtime, completion: Completion) -> Value { + let Completion::Return(value) = completion else { + panic!("expected Completion::Return"); + }; + runtime.root_and_release_jsvalue(value).unwrap() +} + +#[track_caller] +fn thrown(runtime: &Runtime, completion: Completion) -> Value { + let Completion::Throw(value) = completion else { + panic!("expected Completion::Throw"); + }; + runtime.root_and_release_jsvalue(value).unwrap() +} + mod registration; mod code_points; diff --git a/src/engine/builtins/string/tests/case_conversion.rs b/src/engine/builtins/string/tests/case_conversion.rs index 31a28ec0..3491f7b3 100644 --- a/src/engine/builtins/string/tests/case_conversion.rs +++ b/src/engine/builtins/string/tests/case_conversion.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::heap::{AutoInitProperty, PropertySlot}; use crate::engine::object::shape::PropertyFlags; @@ -23,11 +24,21 @@ fn string_case_family_is_ordered_autoinit_and_has_distinct_stable_functions() { let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_indices = keys - .each_ref() - .map(|(_, _, key)| usize::try_from(shape.find(key.atom()).unwrap()).unwrap()); - let value_of_slot = usize::try_from(shape.find(value_of.atom()).unwrap()).unwrap(); - let iterator_slot = usize::try_from(shape.find(iterator.atom()).unwrap()).unwrap(); + let slot_indices = keys.each_ref().map(|(_, _, key)| { + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap() + }); + let value_of_slot = usize::try_from( + shape + .find(AtomIdx::from_raw(value_of.atom().raw())) + .unwrap(), + ) + .unwrap(); + let iterator_slot = usize::try_from( + shape + .find(AtomIdx::from_raw(iterator.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!( slot_indices[0], value_of_slot + 1, @@ -149,17 +160,22 @@ fn string_case_methods_coerce_only_the_receiver_and_ignore_every_argument() { fn string_case_expansion_limit_uses_internal_error_and_accepts_exact_boundary() { let runtime = Runtime::new(); let mut context = runtime.new_context(); - let Completion::Throw(Value::Object(error)) = runtime - .call_string_prototype_case_with_limit( - context.realm, - StringCaseKind::Upper, - NativeInvocation::Call { - this_value: Value::String(JsString::try_from_utf8("ß").unwrap()), - }, - 1, - ) - .unwrap() - else { + let Value::Object(error) = thrown( + &runtime, + runtime + .call_string_prototype_case_with_limit( + context.realm, + StringCaseKind::Upper, + NativeInvocation::Call { + this_value: js( + &runtime, + Value::String(JsString::try_from_utf8("ß").unwrap()), + ), + }, + 1, + ) + .unwrap(), + ) else { panic!("one-below-boundary uppercase conversion did not throw an Error object"); }; for (name, expected) in [("name", "InternalError"), ("message", "string too long")] { @@ -172,17 +188,23 @@ fn string_case_expansion_limit_uses_internal_error_and_accepts_exact_boundary() assert_eq!(value, JsString::from_static(expected)); } assert_eq!( - runtime - .call_string_prototype_case_with_limit( - context.realm, - StringCaseKind::Upper, - NativeInvocation::Call { - this_value: Value::String(JsString::try_from_utf8("ß").unwrap()), - }, - 2, - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("SS"))), + returned( + &runtime, + runtime + .call_string_prototype_case_with_limit( + context.realm, + StringCaseKind::Upper, + NativeInvocation::Call { + this_value: js( + &runtime, + Value::String(JsString::try_from_utf8("ß").unwrap()), + ), + }, + 2, + ) + .unwrap(), + ), + Value::String(JsString::from_static("SS")), "the exact uppercase expansion boundary was rejected", ); } diff --git a/src/engine/builtins/string/tests/construction.rs b/src/engine/builtins/string/tests/construction.rs index 90853672..1e954010 100644 --- a/src/engine/builtins/string/tests/construction.rs +++ b/src/engine/builtins/string/tests/construction.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::builtins::ErrorKind; use crate::engine::heap::{AutoInitProperty, PropertySlot}; use crate::engine::object::shape::PropertyFlags; @@ -24,7 +25,8 @@ fn string_constructor_statics_remain_typed_autoinit_entries() { let state = runtime.0.state.borrow(); let object = state.heap.object(string_constructor.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -66,12 +68,12 @@ fn string_raw_latched_overflow_preserves_pinned_observable_order() { context.realm, &NativeArguments { actual_arg_count: 1, - readable: vec![Value::Object(cooked)], + readable: vec![js(&runtime, Value::Object(cooked))], }, 1, ) .unwrap(); - assert!(matches!(completion, Completion::Throw(Value::Int(77)))); + assert!(matches!(completion, Completion::Throw(JsValue::Int(77)))); assert_eq!( context.eval("stringRawOverflowLog").unwrap(), Value::String(JsString::from_static("g1")), @@ -99,7 +101,10 @@ fn string_raw_latched_overflow_preserves_pinned_observable_order() { context.realm, &NativeArguments { actual_arg_count: 2, - readable: vec![Value::Object(cooked), substitution], + readable: vec![ + js(&runtime, Value::Object(cooked)), + js(&runtime, substitution), + ], }, 1, ) diff --git a/src/engine/builtins/string/tests/html.rs b/src/engine/builtins/string/tests/html.rs index e1726e9b..f52fbc88 100644 --- a/src/engine/builtins/string/tests/html.rs +++ b/src/engine/builtins/string/tests/html.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::heap::{AutoInitProperty, PropertySlot}; use crate::engine::object::shape::PropertyFlags; @@ -24,11 +25,21 @@ fn string_create_html_family_is_ordered_autoinit_and_has_distinct_stable_functio let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_indices = keys - .each_ref() - .map(|(_, _, _, key)| usize::try_from(shape.find(key.atom()).unwrap()).unwrap()); - let iterator_slot = usize::try_from(shape.find(iterator.atom()).unwrap()).unwrap(); - let constructor_slot = usize::try_from(shape.find(constructor.atom()).unwrap()).unwrap(); + let slot_indices = keys.each_ref().map(|(_, _, _, key)| { + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap() + }); + let iterator_slot = usize::try_from( + shape + .find(AtomIdx::from_raw(iterator.atom().raw())) + .unwrap(), + ) + .unwrap(); + let constructor_slot = usize::try_from( + shape + .find(AtomIdx::from_raw(constructor.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!( slot_indices[0], iterator_slot + 1, @@ -338,16 +349,16 @@ fn string_create_html_small_limit_latches_too_long_but_attribute_throw_wins() { context.realm, StringCreateHtmlKind::Anchor, NativeInvocation::Call { - this_value: receiver.clone(), + this_value: runtime.unroot_value(&receiver).unwrap(), }, &NativeArguments { actual_arg_count: 2, - readable: vec![attribute.clone(), extra], + readable: vec![js(&runtime, attribute), js(&runtime, extra)], }, 16, ) .unwrap(); - let Completion::Throw(Value::Object(error)) = completion else { + let Value::Object(error) = thrown(&runtime, completion) else { panic!("one-below-boundary CreateHTML did not throw an Error object"); }; for (name, expected) in [("name", "InternalError"), ("message", "string too long")] { @@ -366,42 +377,48 @@ fn string_create_html_small_limit_latches_too_long_but_attribute_throw_wins() { ); assert_eq!( - runtime - .call_string_prototype_create_html_with_limit( - context.realm, - StringCreateHtmlKind::Anchor, - NativeInvocation::Call { - this_value: Value::String(JsString::from_static("B")), - }, - &NativeArguments { - actual_arg_count: 1, - readable: vec![Value::String(JsString::from_static("Q"))], - }, - 17, - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("B",))), + returned( + &runtime, + runtime + .call_string_prototype_create_html_with_limit( + context.realm, + StringCreateHtmlKind::Anchor, + NativeInvocation::Call { + this_value: js(&runtime, Value::String(JsString::from_static("B"))), + }, + &NativeArguments { + actual_arg_count: 1, + readable: vec![js(&runtime, Value::String(JsString::from_static("Q")),)], + }, + 17, + ) + .unwrap(), + ), + Value::String(JsString::from_static("B",)), "the exact CreateHTML output limit was rejected", ); context.eval("createHtmlLimitLog=''").unwrap(); let throwing_attribute = context.eval("createHtmlLimitThrow").unwrap(); assert_eq!( - runtime - .call_string_prototype_create_html_with_limit( - context.realm, - StringCreateHtmlKind::Anchor, - NativeInvocation::Call { - this_value: receiver, - }, - &NativeArguments { - actual_arg_count: 1, - readable: vec![throwing_attribute], - }, - 1, - ) - .unwrap(), - Completion::Throw(Value::Int(72)), + thrown( + &runtime, + runtime + .call_string_prototype_create_html_with_limit( + context.realm, + StringCreateHtmlKind::Anchor, + NativeInvocation::Call { + this_value: js(&runtime, receiver), + }, + &NativeArguments { + actual_arg_count: 1, + readable: vec![js(&runtime, throwing_attribute)], + }, + 1, + ) + .unwrap(), + ), + Value::Int(72), "CreateHTML's latched TooLong replaced a later user throw", ); assert_eq!( diff --git a/src/engine/builtins/string/tests/padding.rs b/src/engine/builtins/string/tests/padding.rs index 6979adfb..4af9bd17 100644 --- a/src/engine/builtins/string/tests/padding.rs +++ b/src/engine/builtins/string/tests/padding.rs @@ -98,16 +98,16 @@ fn string_pad_small_limit_preserves_filler_order_and_range_error_kind() { context.realm, StringPadKind::End, NativeInvocation::Call { - this_value: Value::String(JsString::from_static("a")), + this_value: js(&runtime, Value::String(JsString::from_static("a"))), }, &NativeArguments { actual_arg_count: 2, - readable: vec![Value::Int(4), filler], + readable: vec![JsValue::Int(4), runtime.into_jsvalue(filler).unwrap()], }, 3, ) .unwrap(); - let Completion::Throw(Value::Object(error)) = completion else { + let Value::Object(error) = thrown(&runtime, completion) else { panic!("small String pad limit did not throw an Error object"); }; for (name, expected) in [("name", "RangeError"), ("message", "invalid string length")] { @@ -126,40 +126,49 @@ fn string_pad_small_limit_preserves_filler_order_and_range_error_kind() { ); assert_eq!( - runtime - .call_string_prototype_pad_with_limit( - context.realm, - StringPadKind::Start, - NativeInvocation::Call { - this_value: Value::String(JsString::from_static("a")), - }, - &NativeArguments { - actual_arg_count: 2, - readable: vec![Value::Int(4), Value::String(JsString::from_static(""))], - }, - 3, - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("a"))), + returned( + &runtime, + runtime + .call_string_prototype_pad_with_limit( + context.realm, + StringPadKind::Start, + NativeInvocation::Call { + this_value: js(&runtime, Value::String(JsString::from_static("a")),), + }, + &NativeArguments { + actual_arg_count: 2, + readable: vec![ + JsValue::Int(4), + js(&runtime, Value::String(JsString::from_static(""))), + ], + }, + 3, + ) + .unwrap(), + ), + Value::String(JsString::from_static("a")), "empty filler must bypass even an otherwise invalid output length", ); assert_eq!( - runtime - .call_string_prototype_pad_with_limit( - context.realm, - StringPadKind::End, - NativeInvocation::Call { - this_value: Value::String(JsString::from_static("a")), - }, - &NativeArguments { - actual_arg_count: 1, - readable: vec![Value::Int(3)], - }, - 3, - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("a "))), + returned( + &runtime, + runtime + .call_string_prototype_pad_with_limit( + context.realm, + StringPadKind::End, + NativeInvocation::Call { + this_value: js(&runtime, Value::String(JsString::from_static("a")),), + }, + &NativeArguments { + actual_arg_count: 1, + readable: vec![JsValue::Int(3)], + }, + 3, + ) + .unwrap(), + ), + Value::String(JsString::from_static("a ")), "the length-one native ABI read a nonexistent filler argument", ); } diff --git a/src/engine/builtins/string/tests/regexp_protocol.rs b/src/engine/builtins/string/tests/regexp_protocol.rs index 9ab766f7..77d74b6c 100644 --- a/src/engine/builtins/string/tests/regexp_protocol.rs +++ b/src/engine/builtins/string/tests/regexp_protocol.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::{NativeCProto, RegExpNativeKind}; use crate::engine::heap::{AutoInitProperty, PropertySlot}; use crate::engine::object::shape::PropertyFlags; @@ -51,13 +52,36 @@ fn match_match_all_search_and_split_entries_preserve_pinned_cproto_and_order() { let state = runtime.0.state.borrow(); let string_object = state.heap.object(string_prototype.object_id()).unwrap(); let string_shape = state.heap.shape(string_object.shape).unwrap(); - let starts_with = usize::try_from(string_shape.find(starts_with.atom()).unwrap()).unwrap(); - let match_position = - usize::try_from(string_shape.find(string_match.atom()).unwrap()).unwrap(); - let match_all = - usize::try_from(string_shape.find(string_match_all.atom()).unwrap()).unwrap(); - let search = usize::try_from(string_shape.find(string_search.atom()).unwrap()).unwrap(); - let split = usize::try_from(string_shape.find(split.atom()).unwrap()).unwrap(); + let starts_with = usize::try_from( + string_shape + .find(AtomIdx::from_raw(starts_with.atom().raw())) + .unwrap(), + ) + .unwrap(); + let match_position = usize::try_from( + string_shape + .find(AtomIdx::from_raw(string_match.atom().raw())) + .unwrap(), + ) + .unwrap(); + let match_all = usize::try_from( + string_shape + .find(AtomIdx::from_raw(string_match_all.atom().raw())) + .unwrap(), + ) + .unwrap(); + let search = usize::try_from( + string_shape + .find(AtomIdx::from_raw(string_search.atom().raw())) + .unwrap(), + ) + .unwrap(); + let split = usize::try_from( + string_shape + .find(AtomIdx::from_raw(split.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!(match_position, starts_with + 1); assert_eq!(match_all, match_position + 1); assert_eq!(search, match_all + 1); @@ -107,12 +131,30 @@ fn match_match_all_search_and_split_entries_preserve_pinned_cproto_and_order() { let regexp_object = state.heap.object(regexp_prototype.object_id()).unwrap(); let regexp_shape = state.heap.shape(regexp_object.shape).unwrap(); - let match_position = - usize::try_from(regexp_shape.find(symbol_match.atom()).unwrap()).unwrap(); - let match_all = - usize::try_from(regexp_shape.find(symbol_match_all.atom()).unwrap()).unwrap(); - let search = usize::try_from(regexp_shape.find(symbol_search.atom()).unwrap()).unwrap(); - let split = usize::try_from(regexp_shape.find(symbol_split.atom()).unwrap()).unwrap(); + let match_position = usize::try_from( + regexp_shape + .find(AtomIdx::from_raw(symbol_match.atom().raw())) + .unwrap(), + ) + .unwrap(); + let match_all = usize::try_from( + regexp_shape + .find(AtomIdx::from_raw(symbol_match_all.atom().raw())) + .unwrap(), + ) + .unwrap(); + let search = usize::try_from( + regexp_shape + .find(AtomIdx::from_raw(symbol_search.atom().raw())) + .unwrap(), + ) + .unwrap(); + let split = usize::try_from( + regexp_shape + .find(AtomIdx::from_raw(symbol_split.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!(match_all, match_position + 1); assert_eq!(search, match_all + 1); assert_eq!(split, search + 1); @@ -355,11 +397,30 @@ fn replace_entries_preserve_pinned_cproto_autoinit_and_table_order() { let state = runtime.0.state.borrow(); let string_object = state.heap.object(string_prototype.object_id()).unwrap(); let string_shape = state.heap.shape(string_object.shape).unwrap(); - let repeat = usize::try_from(string_shape.find(repeat_key.atom()).unwrap()).unwrap(); - let replace = usize::try_from(string_shape.find(replace_key.atom()).unwrap()).unwrap(); - let replace_all = - usize::try_from(string_shape.find(replace_all_key.atom()).unwrap()).unwrap(); - let pad_end = usize::try_from(string_shape.find(pad_end_key.atom()).unwrap()).unwrap(); + let repeat = usize::try_from( + string_shape + .find(AtomIdx::from_raw(repeat_key.atom().raw())) + .unwrap(), + ) + .unwrap(); + let replace = usize::try_from( + string_shape + .find(AtomIdx::from_raw(replace_key.atom().raw())) + .unwrap(), + ) + .unwrap(); + let replace_all = usize::try_from( + string_shape + .find(AtomIdx::from_raw(replace_all_key.atom().raw())) + .unwrap(), + ) + .unwrap(); + let pad_end = usize::try_from( + string_shape + .find(AtomIdx::from_raw(pad_end_key.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!(replace, repeat + 1); assert_eq!(replace_all, replace + 1); assert_eq!(pad_end, replace_all + 1); @@ -395,9 +456,18 @@ fn replace_entries_preserve_pinned_cproto_autoinit_and_table_order() { let regexp_object = state.heap.object(regexp_prototype.object_id()).unwrap(); let regexp_shape = state.heap.shape(regexp_object.shape).unwrap(); - let replace = usize::try_from(regexp_shape.find(symbol_replace.atom()).unwrap()).unwrap(); - let match_position = - usize::try_from(regexp_shape.find(symbol_match.atom()).unwrap()).unwrap(); + let replace = usize::try_from( + regexp_shape + .find(AtomIdx::from_raw(symbol_replace.atom().raw())) + .unwrap(), + ) + .unwrap(); + let match_position = usize::try_from( + regexp_shape + .find(AtomIdx::from_raw(symbol_match.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!(match_position, replace + 1); assert_eq!( regexp_shape.entries()[replace].flags, diff --git a/src/engine/builtins/string/tests/registration.rs b/src/engine/builtins/string/tests/registration.rs index 23abe8ed..8552d9e1 100644 --- a/src/engine/builtins/string/tests/registration.rs +++ b/src/engine/builtins/string/tests/registration.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeCProto; use crate::engine::heap::{AutoInitProperty, PropertySlot, RawValue}; use crate::engine::object::shape::PropertyFlags; @@ -106,10 +107,26 @@ fn string_unicode_intrinsics_use_pinned_generic_cproto_and_append_order() { let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let sup = usize::try_from(shape.find(sup.atom()).unwrap()).unwrap(); - let constructor = usize::try_from(shape.find(constructor.atom()).unwrap()).unwrap(); - let normalize = usize::try_from(shape.find(normalize.atom()).unwrap()).unwrap(); - let locale_compare = usize::try_from(shape.find(locale_compare.atom()).unwrap()).unwrap(); + let sup = + usize::try_from(shape.find(AtomIdx::from_raw(sup.atom().raw())).unwrap()).unwrap(); + let constructor = usize::try_from( + shape + .find(AtomIdx::from_raw(constructor.atom().raw())) + .unwrap(), + ) + .unwrap(); + let normalize = usize::try_from( + shape + .find(AtomIdx::from_raw(normalize.atom().raw())) + .unwrap(), + ) + .unwrap(); + let locale_compare = usize::try_from( + shape + .find(AtomIdx::from_raw(locale_compare.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!(constructor, sup + 1); assert_eq!(normalize, constructor + 1); assert_eq!(locale_compare, normalize + 1); @@ -190,7 +207,8 @@ fn string_subrange_family_publishes_generic_autoinit_entries_and_identities() { let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); for (name, selector, key) in &keys { - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -236,7 +254,8 @@ fn string_repeat_publishes_one_generic_autoinit_entry() { let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -285,9 +304,9 @@ fn string_pad_family_publishes_pinned_autoinit_entries_and_identities() { let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_indices = keys - .each_ref() - .map(|(_, _, key)| usize::try_from(shape.find(key.atom()).unwrap()).unwrap()); + let slot_indices = keys.each_ref().map(|(_, _, key)| { + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap() + }); assert!( slot_indices[0] < slot_indices[1], "padEnd must precede padStart" @@ -352,9 +371,9 @@ fn string_trim_family_preserves_alias_materialization_order_and_independence() { let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let slot_indices = keys - .each_ref() - .map(|(_, _, key)| usize::try_from(shape.find(key.atom()).unwrap()).unwrap()); + let slot_indices = keys.each_ref().map(|(_, _, key)| { + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap() + }); assert!( slot_indices.windows(2).all(|pair| pair[1] == pair[0] + 1), "the five trim-family entries did not retain QuickJS table order", diff --git a/src/engine/builtins/string/tests/search.rs b/src/engine/builtins/string/tests/search.rs index e9e8de19..708ca66a 100644 --- a/src/engine/builtins/string/tests/search.rs +++ b/src/engine/builtins/string/tests/search.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::heap::{AutoInitProperty, PropertySlot}; use crate::engine::object::shape::PropertyFlags; @@ -90,7 +91,8 @@ fn string_includes_family_publishes_typed_autoinit_entries_and_identities() { let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); for (name, selector, key) in &keys { - let slot_index = usize::try_from(shape.find(key.atom()).unwrap()).unwrap(); + let slot_index = + usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).unwrap()).unwrap(); assert_eq!( shape.entries()[slot_index].flags, PropertyFlags::data(true, false, true), @@ -161,25 +163,28 @@ fn string_includes_preserves_pinned_values_utf16_and_shared_magic_kernel() { (StringIncludesKind::EndsWith, "bc", None, true), (StringIncludesKind::EndsWith, "ab", Some(2), true), ] { - let mut readable = vec![Value::String(JsString::from_static(search))]; + let mut readable = vec![js(&runtime, Value::String(JsString::from_static(search)))]; if let Some(position) = position { - readable.push(Value::Int(position)); + readable.push(JsValue::Int(position)); } assert_eq!( - runtime - .call_string_prototype_includes( - context.realm, - selector, - NativeInvocation::Call { - this_value: Value::String(JsString::from_static("abc")), - }, - &NativeArguments { - actual_arg_count: readable.len(), - readable, - }, - ) - .unwrap(), - Completion::Return(Value::Bool(expected)), + returned( + &runtime, + runtime + .call_string_prototype_includes( + context.realm, + selector, + NativeInvocation::Call { + this_value: js(&runtime, Value::String(JsString::from_static("abc")),), + }, + &NativeArguments { + actual_arg_count: readable.len(), + readable, + }, + ) + .unwrap(), + ), + Value::Bool(expected), ); } } diff --git a/src/engine/builtins/string/tests/split.rs b/src/engine/builtins/string/tests/split.rs index a4a18511..6041cfa6 100644 --- a/src/engine/builtins/string/tests/split.rs +++ b/src/engine/builtins/string/tests/split.rs @@ -1,3 +1,4 @@ +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeCProto; use crate::engine::heap::{AutoInitProperty, PropertySlot}; use crate::engine::object::shape::PropertyFlags; @@ -20,9 +21,24 @@ fn string_split_is_a_pinned_generic_autoinit_between_search_and_substring() { let state = runtime.0.state.borrow(); let object = state.heap.object(prototype.object_id()).unwrap(); let shape = state.heap.shape(object.shape).unwrap(); - let search = usize::try_from(shape.find(search_key.atom()).unwrap()).unwrap(); - let split = usize::try_from(shape.find(split_key.atom()).unwrap()).unwrap(); - let substring = usize::try_from(shape.find(substring.atom()).unwrap()).unwrap(); + let search = usize::try_from( + shape + .find(AtomIdx::from_raw(search_key.atom().raw())) + .unwrap(), + ) + .unwrap(); + let split = usize::try_from( + shape + .find(AtomIdx::from_raw(split_key.atom().raw())) + .unwrap(), + ) + .unwrap(); + let substring = usize::try_from( + shape + .find(AtomIdx::from_raw(substring.atom().raw())) + .unwrap(), + ) + .unwrap(); assert_eq!(split, search + 1); assert_eq!(substring, split + 1); assert_eq!( diff --git a/src/engine/builtins/string/tests/subranges.rs b/src/engine/builtins/string/tests/subranges.rs index f5c3ecd6..f1c36f77 100644 --- a/src/engine/builtins/string/tests/subranges.rs +++ b/src/engine/builtins/string/tests/subranges.rs @@ -56,19 +56,17 @@ fn string_subrange_preserves_pinned_clamps_utf16_and_rope_copying() { context.realm, StringSubrangeKind::Slice, NativeInvocation::Call { - this_value: Value::String(rope), + this_value: js(&runtime, Value::String(rope)), }, &NativeArguments { actual_arg_count: 2, - readable: vec![Value::Int(4_999), Value::Int(5_002)], + readable: vec![JsValue::Int(4_999), JsValue::Int(5_002)], }, ) .unwrap(); assert_eq!( - completion, - Completion::Return(Value::String( - JsString::try_from_utf16([0xd83d, 0xde00, u16::from(b'b')]).unwrap() - )) + returned(&runtime, completion), + Value::String(JsString::try_from_utf16([0xd83d, 0xde00, u16::from(b'b')]).unwrap()) ); } diff --git a/src/engine/builtins/string/tests/trimming.rs b/src/engine/builtins/string/tests/trimming.rs index 454dd357..403e2081 100644 --- a/src/engine/builtins/string/tests/trimming.rs +++ b/src/engine/builtins/string/tests/trimming.rs @@ -69,16 +69,18 @@ fn string_trim_preserves_whitespace_sides_utf16_rope_identity_and_argument_ignor } let unchanged = JsString::try_from_utf16([0xd800, 0x20, 0x61, 0xdc00]).unwrap(); - let Completion::Return(Value::String(identity)) = runtime - .call_string_prototype_trim( - context.realm, - StringTrimKind::Both, - NativeInvocation::Call { - this_value: Value::String(unchanged.clone()), - }, - ) - .unwrap() - else { + let Value::String(identity) = returned( + &runtime, + runtime + .call_string_prototype_trim( + context.realm, + StringTrimKind::Both, + NativeInvocation::Call { + this_value: js(&runtime, Value::String(unchanged.clone())), + }, + ) + .unwrap(), + ) else { panic!("identity trim did not return a String"); }; assert!( @@ -102,16 +104,18 @@ fn string_trim_preserves_whitespace_sides_utf16_rope_identity_and_argument_ignor .unwrap(); let rope = left.try_concat(&right).unwrap(); assert!(!rope.is_flat()); - let Completion::Return(Value::String(trimmed)) = runtime - .call_string_prototype_trim( - context.realm, - StringTrimKind::Both, - NativeInvocation::Call { - this_value: Value::String(rope), - }, - ) - .unwrap() - else { + let Value::String(trimmed) = returned( + &runtime, + runtime + .call_string_prototype_trim( + context.realm, + StringTrimKind::Both, + NativeInvocation::Call { + this_value: js(&runtime, Value::String(rope)), + }, + ) + .unwrap(), + ) else { panic!("rope trim did not return a String"); }; assert!(trimmed.is_flat()); diff --git a/src/engine/builtins/string/tests/unicode.rs b/src/engine/builtins/string/tests/unicode.rs index e9f2cd76..28ebc4c0 100644 --- a/src/engine/builtins/string/tests/unicode.rs +++ b/src/engine/builtins/string/tests/unicode.rs @@ -281,19 +281,24 @@ fn string_normalize_limit_and_oom_use_internal_error_and_recover() { let arguments = NativeArguments { actual_arg_count: 1, - readable: vec![Value::String(JsString::from_static("NFD"))], + readable: vec![js(&runtime, Value::String(JsString::from_static("NFD")))], }; - let Completion::Throw(Value::Object(error)) = runtime - .call_string_prototype_normalize_with_limit( - defining.realm, - NativeInvocation::Call { - this_value: Value::String(JsString::try_from_utf8("ý").unwrap()), - }, - &arguments, - 1, - ) - .unwrap() - else { + let Value::Object(error) = thrown( + &runtime, + runtime + .call_string_prototype_normalize_with_limit( + defining.realm, + NativeInvocation::Call { + this_value: js( + &runtime, + Value::String(JsString::try_from_utf8("ý").unwrap()), + ), + }, + &arguments, + 1, + ) + .unwrap(), + ) else { panic!("one-below-boundary normalization did not throw an Error object"); }; for (name, expected) in [("name", "InternalError"), ("message", "string too long")] { @@ -306,19 +311,23 @@ fn string_normalize_limit_and_oom_use_internal_error_and_recover() { assert_eq!(value, JsString::from_static(expected)); } assert_eq!( - runtime - .call_string_prototype_normalize_with_limit( - defining.realm, - NativeInvocation::Call { - this_value: Value::String(JsString::try_from_utf8("ý").unwrap()), - }, - &arguments, - 2, - ) - .unwrap(), - Completion::Return(Value::String( - JsString::try_from_utf16([u16::from(b'y'), 0x0301]).unwrap(), - )), + returned( + &runtime, + runtime + .call_string_prototype_normalize_with_limit( + defining.realm, + NativeInvocation::Call { + this_value: js( + &runtime, + Value::String(JsString::try_from_utf8("ý").unwrap()), + ), + }, + &arguments, + 2, + ) + .unwrap(), + ), + Value::String(JsString::try_from_utf16([u16::from(b'y'), 0x0301]).unwrap()), "the exact normalization expansion boundary was rejected", ); diff --git a/src/engine/builtins/string/text.rs b/src/engine/builtins/string/text.rs index 5fb0e38d..90cad5e7 100644 --- a/src/engine/builtins/string/text.rs +++ b/src/engine/builtins/string/text.rs @@ -6,7 +6,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, builtins::native::{StringCaseKind, StringCreateHtmlKind, StringPadKind, StringTrimKind}, heap::ContextId, - value::{CreateHtmlStringBuffer, JsString, Value, conversion::NativeConversion}, + value::{CreateHtmlStringBuffer, JsString, JsValue, Value, conversion::NativeConversion}, vm::{ Completion, ToPrimitiveHint, call::{NativeArguments, NativeInvocation}, @@ -40,7 +40,7 @@ impl StringTextKind { pub(crate) enum StringTextStep { Complete(Completion), Primitive { - value: Value, + value: JsValue, hint: ToPrimitiveHint, resume: StringTextResume, }, @@ -113,29 +113,32 @@ impl StringTextStep { "String text conversion did not receive a call", )); }; + let this_value = runtime.root_value(this_value)?; if matches!(this_value, Value::Undefined | Value::Null) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "null or undefined are forbidden", )?, ))); } + let first = match arguments.and_then(|args| args.readable.first()) { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + }; + let second = match arguments.and_then(|args| args.readable.get(1)) { + Some(value) => runtime.root_value(value)?, + None => Value::Undefined, + }; Ok(Self::Primitive { - value: this_value.clone(), + value: runtime.into_jsvalue(this_value)?, hint: ToPrimitiveHint::String, resume: StringTextResume(Box::new(StringTextResumeState { realm, kind, - first: arguments - .and_then(|args| args.readable.first()) - .cloned() - .unwrap_or(Value::Undefined), - second: arguments - .and_then(|args| args.readable.get(1)) - .cloned() - .unwrap_or(Value::Undefined), + first, + second, actual: arguments.map_or(0, |args| args.actual_arg_count), limit, phase: TextPhase::Source, @@ -144,7 +147,12 @@ impl StringTextStep { } } impl StringTextResume { - fn convert(mut self, value: Value, hint: ToPrimitiveHint, phase: TextPhase) -> StringTextStep { + fn convert( + mut self, + value: JsValue, + hint: ToPrimitiveHint, + phase: TextPhase, + ) -> StringTextStep { StringTextStep::Primitive { value, hint, @@ -161,7 +169,7 @@ impl StringTextResume { result: Completion, ) -> Result { let value = match result { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { return Ok(StringTextStep::Complete(Completion::Throw(value))); } @@ -177,7 +185,9 @@ impl StringTextResume { let source = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; match self.0.kind { @@ -188,7 +198,7 @@ impl StringTextResume { runtime.finish_string_case(realm, kind, source, self.0.limit)? } StringTextKind::Repeat => { - let argument = self.0.first.clone(); + let argument = runtime.unroot_value(&self.0.first)?; return Ok(self.convert( argument, ToPrimitiveHint::Number, @@ -196,7 +206,7 @@ impl StringTextResume { )); } StringTextKind::Pad(_) => { - let argument = self.0.first.clone(); + let argument = runtime.unroot_value(&self.0.first)?; return Ok(self.convert( argument, ToPrimitiveHint::Number, @@ -212,7 +222,7 @@ impl StringTextResume { self.0.limit, )? } else { - let argument = self.0.first.clone(); + let argument = runtime.unroot_value(&self.0.first)?; return Ok(self.convert( argument, ToPrimitiveHint::String, @@ -221,7 +231,7 @@ impl StringTextResume { } } StringTextKind::LocaleCompare => { - let argument = self.0.first.clone(); + let argument = runtime.unroot_value(&self.0.first)?; return Ok(self.convert( argument, ToPrimitiveHint::String, @@ -235,14 +245,14 @@ impl StringTextResume { if attribute.is_some() { if matches!(self.0.first, Value::Undefined | Value::Null) { return Ok(StringTextStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "null or undefined are forbidden", )?, ))); } - let argument = self.0.first.clone(); + let argument = runtime.unroot_value(&self.0.first)?; return Ok(self.convert( argument, ToPrimitiveHint::String, @@ -261,7 +271,9 @@ impl StringTextResume { let count = match runtime.native_to_int64_sat(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; runtime.finish_string_repeat(realm, source, count, self.0.limit)? @@ -272,15 +284,17 @@ impl StringTextResume { crate::engine::value::number::to_int32_sat(value) } NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let source_len = i32::try_from(source.len()) .map_err(|_| RuntimeError::Invariant("String length exceeded signed Int32"))?; if source_len >= target { - Completion::Return(Value::String(source)) + Completion::Return(runtime.into_jsvalue(Value::String(source))?) } else if self.0.actual > 1 && !matches!(self.0.second, Value::Undefined) { - let argument = self.0.second.clone(); + let argument = runtime.unroot_value(&self.0.second)?; return Ok({ let updated_0 = TextPhase::Source; self.0.phase = updated_0; @@ -302,7 +316,9 @@ impl StringTextResume { let filler = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value.linearize(), NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let StringTextKind::Pad(kind) = self.0.kind else { @@ -321,7 +337,9 @@ impl StringTextResume { let form = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let form = if form.utf16_units().eq("NFC".encode_utf16()) { @@ -334,7 +352,7 @@ impl StringTextResume { NormalizationForm::Nfkd } else { return Ok(StringTextStep::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Range, "bad normalization form", @@ -347,7 +365,9 @@ impl StringTextResume { let that = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; runtime.finish_string_locale_compare(realm, source, that)? @@ -360,7 +380,9 @@ impl StringTextResume { let attribute = match runtime.native_to_js_string(realm, &value)? { NativeConversion::Value(value) => value.linearize(), NativeConversion::Throw(value) => { - return Ok(StringTextStep::Complete(Completion::Throw(value))); + return Ok(StringTextStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; buffer.append_escaped_attribute(&attribute); @@ -383,8 +405,8 @@ pub(super) fn finish( hint, resume, } => { - let result = if matches!(value, Value::Object(_)) { - runtime.to_primitive(realm, value, hint)? + let result = if matches!(value, JsValue::Object(_)) { + runtime.to_primitive_jsvalue(realm, value, hint)? } else { Completion::Return(value) }; diff --git a/src/engine/builtins/weak_collection.rs b/src/engine/builtins/weak_collection.rs index c98135c2..f0320aaf 100644 --- a/src/engine/builtins/weak_collection.rs +++ b/src/engine/builtins/weak_collection.rs @@ -19,7 +19,7 @@ use crate::engine::object::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -281,17 +281,18 @@ impl Runtime { ) } - fn weak_collection_receiver<'a>( + fn weak_collection_receiver( &self, realm: ContextId, - invocation: &'a NativeInvocation, + invocation: &NativeInvocation, kind: WeakCollectionKind, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { let NativeInvocation::Call { this_value } = invocation else { return Err(RuntimeError::Invariant( "weak collection method received the wrong native invocation", )); }; + let this_value = self.root_value(this_value)?; let Value::Object(object) = this_value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, @@ -356,7 +357,7 @@ impl Runtime { realm: ContextId, kind: WeakCollectionKind, ) -> Result { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, &format!("invalid value used as {} key", kind.name()), @@ -385,6 +386,9 @@ impl Runtime { ) -> Result<(), RuntimeError> { self.validate_value_domain(&value, "WeakMap value")?; let raw_value = self.raw_property_value(&value)?; + // The record retains its own copy edge inside the heap transaction, + // so the conversion's producer edge is released on every exit. + let conversion_edge = raw_value.conversion_node_edge(); let mut state = self.0.state.borrow_mut(); let retained = state.retain_raw_value_atoms([&raw_value])?; let result = state.heap.weak_map_set(map.object_id(), key, raw_value); @@ -392,11 +396,18 @@ impl Runtime { Ok(cleanup) => cleanup, Err(error) => { state.release_atoms(retained)?; + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } return Err(Self::weak_collection_mutation_error(error)); } }; state.apply_cleanup(cleanup)?; drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } drop(value); Ok(()) } @@ -459,7 +470,9 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.call_weak_map_native_borrowed(realm, kind, &invocation, arguments) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.call_weak_map_native_borrowed(realm, kind, invocation, arguments) + }) } pub(crate) fn call_weak_map_native_borrowed( &self, @@ -485,13 +498,16 @@ impl Runtime { } let map = match self.weak_collection_receiver(realm, invocation, WeakCollectionKind::Map)? { NativeConversion::Value(map) => map, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let key_value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant("WeakMap key argv was not padded"))?; + let key_value = self.root_value( + arguments + .readable + .first() + .ok_or(RuntimeError::Invariant("WeakMap key argv was not padded"))?, + )?; let key = self.weak_collection_key(&key_value, "WeakMap key")?; match kind { @@ -499,48 +515,52 @@ impl Runtime { let Some(key) = key else { return self.invalid_weak_key(realm, WeakCollectionKind::Map); }; - let value = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant("WeakMap value argv was not padded"))?; - self.set_weak_map_record(map, key, value)?; - Ok(Completion::Return(Value::Object(map.clone()))) + let value = self.root_value( + arguments + .readable + .get(1) + .ok_or(RuntimeError::Invariant("WeakMap value argv was not padded"))?, + )?; + self.set_weak_map_record(&map, key, value)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object(map))?)) } WeakMapNativeKind::Get => { let value = match key { - Some(key) => match self.find_weak_map_record(map, key)? { + Some(key) => match self.find_weak_map_record(&map, key)? { Some(value) => self.root_raw_value(&value)?, None => Value::Undefined, }, None => Value::Undefined, }; - Ok(Completion::Return(value)) + Ok(Completion::Return(self.into_jsvalue(value)?)) } WeakMapNativeKind::GetOrInsert => { let Some(key) = key else { return self.invalid_weak_key(realm, WeakCollectionKind::Map); }; - if let Some(value) = self.find_weak_map_record(map, key)? { - return Ok(Completion::Return(self.root_raw_value(&value)?)); + if let Some(value) = self.find_weak_map_record(&map, key)? { + return Ok(Completion::Return( + self.into_jsvalue(self.root_raw_value(&value)?)?, + )); } - let value = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant("WeakMap value argv was not padded"))?; - self.set_weak_map_record(map, key, value.clone())?; - Ok(Completion::Return(value)) + let value = self.root_value( + arguments + .readable + .get(1) + .ok_or(RuntimeError::Invariant("WeakMap value argv was not padded"))?, + )?; + self.set_weak_map_record(&map, key, value.clone())?; + Ok(Completion::Return(self.into_jsvalue(value)?)) } WeakMapNativeKind::Has => { let present = match key { - Some(key) => self.find_weak_map_record(map, key)?.is_some(), + Some(key) => self.find_weak_map_record(&map, key)?.is_some(), None => false, }; - Ok(Completion::Return(Value::Bool(present))) + Ok(Completion::Return(JsValue::Bool(present))) } - WeakMapNativeKind::Delete => Ok(Completion::Return(Value::Bool(match key { - Some(key) => self.delete_weak_map_record(map, key)?, + WeakMapNativeKind::Delete => Ok(Completion::Return(JsValue::Bool(match key { + Some(key) => self.delete_weak_map_record(&map, key)?, None => false, }))), WeakMapNativeKind::Constructor | WeakMapNativeKind::GetOrInsertComputed => { @@ -556,7 +576,9 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - self.call_weak_set_native_borrowed(realm, kind, &invocation, arguments) + self.dispatch_borrowed_invocation(invocation, |invocation| { + self.call_weak_set_native_borrowed(realm, kind, invocation, arguments) + }) } pub(crate) fn call_weak_set_native_borrowed( &self, @@ -575,28 +597,31 @@ impl Runtime { } let set = match self.weak_collection_receiver(realm, invocation, WeakCollectionKind::Set)? { NativeConversion::Value(set) => set, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let key_value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant("WeakSet value argv was not padded"))?; + let key_value = self.root_value( + arguments + .readable + .first() + .ok_or(RuntimeError::Invariant("WeakSet value argv was not padded"))?, + )?; let key = self.weak_collection_key(&key_value, "WeakSet key")?; match kind { WeakSetNativeKind::Add => { let Some(key) = key else { return self.invalid_weak_key(realm, WeakCollectionKind::Set); }; - self.insert_weak_set_record(set, key)?; - Ok(Completion::Return(Value::Object(set.clone()))) + self.insert_weak_set_record(&set, key)?; + Ok(Completion::Return(self.into_jsvalue(Value::Object(set))?)) } - WeakSetNativeKind::Has => Ok(Completion::Return(Value::Bool(match key { - Some(key) => self.has_weak_set_record(set, key)?, + WeakSetNativeKind::Has => Ok(Completion::Return(JsValue::Bool(match key { + Some(key) => self.has_weak_set_record(&set, key)?, None => false, }))), - WeakSetNativeKind::Delete => Ok(Completion::Return(Value::Bool(match key { - Some(key) => self.delete_weak_set_record(set, key)?, + WeakSetNativeKind::Delete => Ok(Completion::Return(JsValue::Bool(match key { + Some(key) => self.delete_weak_set_record(&set, key)?, None => false, }))), WeakSetNativeKind::Constructor => { diff --git a/src/engine/builtins/weak_collection/computed.rs b/src/engine/builtins/weak_collection/computed.rs index 7d869590..d8949ecd 100644 --- a/src/engine/builtins/weak_collection/computed.rs +++ b/src/engine/builtins/weak_collection/computed.rs @@ -4,7 +4,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, WeakCollectionKey}, object::{CallableRef, ObjectRef}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -14,7 +14,7 @@ pub(crate) enum ComputedStep { Complete(Completion), Call { callable: CallableRef, - arguments: Vec, + arguments: Vec, resume: ComputedResume, }, } @@ -47,28 +47,27 @@ impl ComputedStep { match runtime.weak_collection_receiver(realm, invocation, WeakCollectionKind::Map)? { NativeConversion::Value(map) => map, NativeConversion::Throw(value) => { - return Ok(Self::Complete(Completion::Throw(value))); + return Ok(Self::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; - let key_value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant("WeakMap key argv was not padded"))?; - let callback_value = arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "WeakMap computed value argv was not padded", - ))?; + let key_value = runtime.root_value( + arguments + .readable + .first() + .ok_or(RuntimeError::Invariant("WeakMap key argv was not padded"))?, + )?; + let callback_value = runtime.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("WeakMap computed value argv was not padded"), + )?)?; let callback = match callback_value { Value::Object(ref object) => runtime.as_callable(object)?, _ => None, }; let Some(callable) = callback else { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Type, "not a function")?, ))); }; let Some(key) = runtime.weak_collection_key(&key_value, "WeakMap key")? else { @@ -76,14 +75,14 @@ impl ComputedStep { runtime.invalid_weak_key(realm, WeakCollectionKind::Map)?, )); }; - if let Some(value) = runtime.find_weak_map_record(map, key)? { + if let Some(value) = runtime.find_weak_map_record(&map, key)? { return Ok(Self::Complete(Completion::Return( - runtime.root_raw_value(&value)?, + runtime.into_jsvalue(runtime.root_raw_value(&value)?)?, ))); } Ok(Self::Call { callable, - arguments: vec![key_value.clone()], + arguments: vec![runtime.into_jsvalue(key_value.clone())?], resume: ComputedResume(Box::new(ComputedResumeState { map: map.clone(), key, @@ -101,9 +100,12 @@ impl ComputedResume { match reply { Completion::Throw(value) => Ok(ComputedStep::Complete(Completion::Throw(value))), Completion::Return(value) => { + let value = runtime.root_and_release_jsvalue(value)?; runtime.delete_weak_map_record(&self.0.map, self.0.key)?; runtime.set_weak_map_record(&self.0.map, self.0.key, value.clone())?; - Ok(ComputedStep::Complete(Completion::Return(value))) + Ok(ComputedStep::Complete(Completion::Return( + runtime.into_jsvalue(value)?, + ))) } } } @@ -120,10 +122,16 @@ pub(crate) fn finish( callable, arguments, resume, - } => resume.resume( - runtime, - runtime.call_internal(realm, &callable, Value::Undefined, &arguments)?, - )?, + } => { + let arguments = arguments + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; + resume.resume( + runtime, + runtime.call_internal(realm, &callable, Value::Undefined, &arguments)?, + )? + } }; } } diff --git a/src/engine/builtins/weak_ref.rs b/src/engine/builtins/weak_ref.rs index b8dc3431..7388d788 100644 --- a/src/engine/builtins/weak_ref.rs +++ b/src/engine/builtins/weak_ref.rs @@ -7,7 +7,7 @@ use crate::engine::api::error::{Error, ErrorKind, NativeErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::AtomKind; +use crate::engine::atom::{AtomIdx, AtomKind}; use crate::engine::builtins::native::{ FinalizationRegistryNativeKind, NativeFunctionId, WeakRefNativeKind, @@ -20,7 +20,7 @@ use crate::engine::object::{ WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; @@ -203,7 +203,7 @@ impl Runtime { realm: ContextId, message: &'static str, ) -> Result { - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, message, @@ -296,6 +296,7 @@ impl Runtime { "WeakRef.prototype.deref received the wrong native invocation", )); }; + let this_value = self.root_value(this_value)?; let Value::Object(weak_ref) = this_value else { return self.invalid_weak_target(realm, "WeakRef object expected"); }; @@ -314,7 +315,7 @@ impl Runtime { Err(error) => return Err(error.into()), }; let Some(target) = target else { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); }; let live = { let state = self.0.state.borrow(); @@ -330,27 +331,34 @@ impl Runtime { } }; if !live { - return Ok(Completion::Return(Value::Undefined)); + return Ok(Completion::Return(JsValue::Undefined)); } let raw = match target { WeakCollectionKey::Object(object) => RawValue::Object(object), - WeakCollectionKey::Symbol(atom) => RawValue::Symbol(atom), + // The branded key atom was already validated by the heap + // lookup above, so it can be narrowed without re-branding. + WeakCollectionKey::Symbol(atom) => { + RawValue::Symbol(AtomIdx::from_raw(atom.raw())) + } }; - Ok(Completion::Return(self.root_raw_value(&raw)?)) + Ok(Completion::Return( + self.into_jsvalue(self.root_raw_value(&raw)?)?, + )) } } } - pub(in crate::engine::builtins) fn finalization_registry_receiver<'a>( + pub(in crate::engine::builtins) fn finalization_registry_receiver( &self, realm: ContextId, - invocation: &'a NativeInvocation, - ) -> Result, RuntimeError> { + invocation: &NativeInvocation, + ) -> Result, RuntimeError> { let NativeInvocation::Call { this_value } = invocation else { return Err(RuntimeError::Invariant( "FinalizationRegistry method received the wrong native invocation", )); }; + let this_value = self.root_value(this_value)?; let Value::Object(registry) = this_value else { return Ok(NativeConversion::Throw(self.new_native_error( realm, @@ -403,15 +411,13 @@ impl Runtime { let registry = match self.finalization_registry_receiver(realm, invocation)? { NativeConversion::Value(registry) => registry, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; - let first = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "FinalizationRegistry first argv was not padded", - ))?; + let first = self.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant("FinalizationRegistry first argv was not padded"), + )?)?; match kind { FinalizationRegistryNativeKind::Constructor => { unreachable!("FinalizationRegistry constructor returned before receiver validation") @@ -421,25 +427,16 @@ impl Runtime { else { return self.invalid_weak_target(realm, "invalid target"); }; - let held_value = - arguments - .readable - .get(1) - .cloned() - .ok_or(RuntimeError::Invariant( - "FinalizationRegistry held value argv was not padded", - ))?; + let held_value = self.root_value(arguments.readable.get(1).ok_or( + RuntimeError::Invariant("FinalizationRegistry held value argv was not padded"), + )?)?; if first.same_value(&held_value) { return self.invalid_weak_target(realm, "held value cannot be the target"); } let token_value = - arguments - .readable - .get(2) - .cloned() - .ok_or(RuntimeError::Invariant( - "FinalizationRegistry unregister token argv was not padded", - ))?; + self.root_value(arguments.readable.get(2).ok_or(RuntimeError::Invariant( + "FinalizationRegistry unregister token argv was not padded", + ))?)?; let unregister_token = if matches!(token_value, Value::Undefined) { None } else { @@ -453,6 +450,11 @@ impl Runtime { self.validate_value_domain(&held_value, "FinalizationRegistry held value")?; let raw_held_value = self.raw_property_value(&held_value)?; + // The conversion allocated a string/BigInt node with one + // producer edge; the registry entry retains its own copy edge + // inside `finalization_registry_register`, so the producer + // edge is released on every exit. + let conversion_edge = raw_held_value.conversion_node_edge(); let mut state = self.0.state.borrow_mut(); let retained_atoms = state.retain_raw_value_atoms([&raw_held_value])?; if let Err(error) = state.heap.finalization_registry_register( @@ -462,10 +464,17 @@ impl Runtime { unregister_token, ) { state.release_atoms(retained_atoms)?; + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } return Err(Self::weak_intrinsic_mutation_error(error)); } drop(state); - Ok(Completion::Return(Value::Undefined)) + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } + Ok(Completion::Return(JsValue::Undefined)) } FinalizationRegistryNativeKind::Unregister => { let Some(token) = @@ -478,7 +487,7 @@ impl Runtime { .heap .finalization_registry_unregister(registry.object_id(), token)?; state.apply_cleanup(cleanup)?; - Ok(Completion::Return(Value::Bool(removed))) + Ok(Completion::Return(JsValue::Bool(removed))) } } } diff --git a/src/engine/builtins/weak_ref/constructor.rs b/src/engine/builtins/weak_ref/constructor.rs index c734ce81..051799c4 100644 --- a/src/engine/builtins/weak_ref/constructor.rs +++ b/src/engine/builtins/weak_ref/constructor.rs @@ -4,7 +4,7 @@ use crate::engine::{ api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::{ContextId, WeakCollectionKey}, object::{CallableRef, ObjectRef}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{ @@ -16,7 +16,7 @@ use crate::engine::{ pub(crate) enum WeakConstructorStep { Complete(Completion), Prototype { - new_target: Value, + new_target: JsValue, resume: WeakConstructorResume, }, } @@ -62,25 +62,23 @@ impl WeakConstructorStep { } })); }; - if matches!(new_target, Value::Undefined) { + if matches!(new_target, JsValue::Undefined) { return Ok(Self::Complete(Completion::Throw( - runtime.new_native_error( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "constructor requires 'new'", )?, ))); } - let value = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant(match kind { + let value = runtime.root_value(arguments.readable.first().ok_or( + RuntimeError::Invariant(match kind { WeakIntrinsicKind::WeakRef => "WeakRef target argv was not padded", WeakIntrinsicKind::FinalizationRegistry => { "FinalizationRegistry callback argv was not padded" } - }))?; + }), + )?)?; let input = match kind { WeakIntrinsicKind::WeakRef => { let Some(key) = runtime.weak_target_key(&value, "WeakRef target")? else { @@ -104,7 +102,7 @@ impl WeakConstructorStep { } }; Ok(Self::Prototype { - new_target: new_target.clone(), + new_target: runtime.dup_jsvalue(new_target)?, resume: WeakConstructorResume(Box::new(WeakConstructorResumeState { realm, input })), }) } @@ -117,7 +115,9 @@ impl WeakConstructorResume { ) -> Result { let prototype = match reply { NativeConversion::Throw(value) => { - return Ok(WeakConstructorStep::Complete(Completion::Throw(value))); + return Ok(WeakConstructorStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } NativeConversion::Value(ConstructorPrototypeSource::Explicit(prototype)) => prototype, NativeConversion::Value(ConstructorPrototypeSource::Realm(realm)) => { @@ -138,7 +138,7 @@ impl WeakConstructorResume { } }; Ok(WeakConstructorStep::Complete(Completion::Return( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, ))) } } @@ -150,14 +150,17 @@ pub(super) fn finish( loop { step = match step { WeakConstructorStep::Complete(result) => return Ok(result), - WeakConstructorStep::Prototype { new_target, resume } => resume.prototype( - runtime, - finish_source( + WeakConstructorStep::Prototype { new_target, resume } => { + let new_target = runtime.root_and_release_jsvalue(new_target)?; + resume.prototype( runtime, - realm, - ProtoSourceStep::start(runtime, realm, new_target)?, - )?, - )?, + finish_source( + runtime, + realm, + ProtoSourceStep::start(runtime, realm, new_target)?, + )?, + )? + } }; } } diff --git a/src/engine/code/bytecode_publish.rs b/src/engine/code/bytecode_publish.rs index 97e566c2..08347942 100644 --- a/src/engine/code/bytecode_publish.rs +++ b/src/engine/code/bytecode_publish.rs @@ -40,7 +40,8 @@ pub(crate) fn link_constant_property_keys( keys.resize(index + 1, Atom::NULL); } if keys[index].is_null() { - let atom = state.atoms.intern_property_key_js_string(name)?; + let text = state.heap.string(*name)?.clone(); + let atom = state.atoms.intern_property_key_js_string(&text)?; auxiliary_atoms.push(atom); keys[index] = atom; } @@ -138,7 +139,9 @@ pub(crate) fn flatten_unlinked_tree( .last_mut() .expect("flatten frame remains present") .constants - .push(FlatConstant::Value(raw_unlinked_primitive(value.into())?)), + .push(FlatConstant::Value(validate_unlinked_primitive( + value.into(), + )?)), (None, false, Some(child)) => frames.push(FlattenFrame::new(child)), (None, _, None) | (Some(_), true, None) @@ -176,17 +179,14 @@ pub(crate) fn flatten_unlinked_tree( } } -fn raw_unlinked_primitive(value: Value) -> Result { +/// Primitive constants stay as public `Value` payloads through flattening; +/// the publish transaction in `code::runtime` is their string/BigInt node +/// creation point, so only the runtime-bound escape invariant is sealed here. +fn validate_unlinked_primitive(value: Value) -> Result { match value { - Value::Undefined => Ok(RawValue::Undefined), - Value::Null => Ok(RawValue::Null), - Value::Bool(value) => Ok(RawValue::Bool(value)), - Value::Int(value) => Ok(RawValue::Int(value)), - Value::Float(value) => Ok(RawValue::Float(value)), - Value::BigInt(value) => Ok(RawValue::BigInt(value)), - Value::String(value) => Ok(RawValue::String(value)), Value::Object(_) | Value::Symbol(_) => Err(RuntimeError::Invariant( "runtime-bound value escaped the unlinked constant invariant", )), + value => Ok(value), } } diff --git a/src/engine/code/bytecode_publish/private_elements.rs b/src/engine/code/bytecode_publish/private_elements.rs index 70781745..815631ad 100644 --- a/src/engine/code/bytecode_publish/private_elements.rs +++ b/src/engine/code/bytecode_publish/private_elements.rs @@ -8,7 +8,7 @@ use crate::engine::code::verify::private_elements::{ PrivateBindingRole, private_binding_info, private_setter_local_pairs, }; use crate::engine::heap::{ - BytecodeConstant, PublishedPrivateBinding, PublishedPrivateBindings, RawValue, + BytecodeConstant, Heap, PublishedPrivateBinding, PublishedPrivateBindings, RawValue, }; /// Name-aware publication data retained across atom linking. The unlinked @@ -26,6 +26,7 @@ pub(crate) fn prepare_private_binding_publication( local_definitions: &[UnlinkedVariableDefinition], closure_variables: &[ClosureVariable], constants: &[BytecodeConstant], + heap: &Heap, ) -> Result { let mut local_roles = vec![None; local_definitions.len()]; let local_pairs = private_setter_local_pairs(local_definitions)?; @@ -60,7 +61,7 @@ pub(crate) fn prepare_private_binding_publication( .ok() .and_then(|constant| constants.get(constant)) .and_then(|constant| match constant { - BytecodeConstant::Value(RawValue::String(name)) => Some(name), + BytecodeConstant::Value(RawValue::String(name)) => heap.string(*name).ok(), BytecodeConstant::Value(_) | BytecodeConstant::RegExp { .. } | BytecodeConstant::Function(_) => None, diff --git a/src/engine/code/runtime.rs b/src/engine/code/runtime.rs index 617b52a4..c86050ce 100644 --- a/src/engine/code/runtime.rs +++ b/src/engine/code/runtime.rs @@ -20,7 +20,7 @@ use crate::engine::heap::{ BytecodeConstant, ContextId, FunctionBytecodeData, FunctionDebugInfo, PublishedPrivateBindings, RawValue, }; -use crate::engine::value::JsString; +use crate::engine::value::{JsString, Value}; #[cfg(test)] use crate::source::LineColumn; use std::rc::Rc; @@ -71,47 +71,52 @@ impl Runtime { let mut atom_string_constants = Vec::new(); let mut children = Vec::new(); let mut materialized_constant_roots = Vec::new(); - for constant in function.constants { - match constant { - FlatConstant::Value(value) => { - linked_constants.push(BytecodeConstant::Value(value)); - } - FlatConstant::AtomString(value) => { - atom_string_constants.push(linked_constants.len()); - linked_constants.push(BytecodeConstant::Value(RawValue::String(value))); - } - FlatConstant::RegExp { pattern, program } => { - linked_constants.push(BytecodeConstant::RegExp { pattern, program }); - } - FlatConstant::TemplateObject { cooked, raw } => { - let template = self.instantiate_template_object(realm, cooked, raw)?; - linked_constants.push(BytecodeConstant::Value(RawValue::Object( - template.object_id(), - ))); - materialized_constant_roots.push(template); - } - FlatConstant::Child(index) => { - let child = roots.get(index).and_then(Option::as_ref).ok_or( - RuntimeError::Invariant( - "flattened child function root was unavailable", - ), - )?; - linked_constants.push(BytecodeConstant::Function(child.bytecode_id())); - children.push(index); + let constants_linked = (|| -> Result<(), RuntimeError> { + for constant in function.constants { + match constant { + FlatConstant::Value(value) => { + let raw = self.raw_property_value(&value)?; + linked_constants.push(BytecodeConstant::Value(raw)); + } + FlatConstant::AtomString(value) => { + atom_string_constants.push(linked_constants.len()); + let raw = self.raw_property_value(&Value::String(value))?; + linked_constants.push(BytecodeConstant::Value(raw)); + } + FlatConstant::RegExp { pattern, program } => { + linked_constants.push(BytecodeConstant::RegExp { pattern, program }); + } + FlatConstant::TemplateObject { cooked, raw } => { + let template = self.instantiate_template_object(realm, cooked, raw)?; + linked_constants.push(BytecodeConstant::Value(RawValue::Object( + template.object_id(), + ))); + materialized_constant_roots.push(template); + } + FlatConstant::Child(index) => { + let child = roots.get(index).and_then(Option::as_ref).ok_or( + RuntimeError::Invariant( + "flattened child function root was unavailable", + ), + )?; + linked_constants.push(BytecodeConstant::Function(child.bytecode_id())); + children.push(index); + } } } + Ok(()) + })(); + if let Err(error) = constants_linked { + // No bytecode node will retain these converted constants, so + // drop their caller-owned string/BigInt producer edges now. + release_constant_edges(self, &linked_constants); + return Err(error); } let mut closure_variables = function.closure_variables; let eval_environments = function.eval_environments; let argument_definitions = function.argument_definitions; let local_definitions = function.local_definitions; - let private_binding_publication = - bytecode_publish::prepare_private_binding_publication( - &local_definitions, - &closure_variables, - &linked_constants, - )?; let mut linked_argument_definitions = Vec::with_capacity(argument_definitions.len()); let mut linked_local_definitions = Vec::with_capacity(local_definitions.len()); let mut linked_eval_environments = Vec::with_capacity(eval_environments.len()); @@ -125,7 +130,9 @@ impl Runtime { let linking = (|| -> Result<(), RuntimeError> { for index in atom_string_constants { let value = match linked_constants.get(index) { - Some(BytecodeConstant::Value(RawValue::String(value))) => value.clone(), + Some(BytecodeConstant::Value(RawValue::String(value))) => { + state.heap.string(*value)?.clone() + } Some(BytecodeConstant::Value(_)) | Some(BytecodeConstant::RegExp { .. }) | Some(BytecodeConstant::Function(_)) @@ -143,8 +150,17 @@ impl Runtime { } auxiliary_atoms.push(atom); let canonical = state.atoms.to_js_string(atom)?; - linked_constants[index] = - BytecodeConstant::Value(RawValue::String(canonical)); + let canonical = state.heap.allocate_string(canonical)?; + // The replaced draft string is never stored, so its + // producer edge dies here; the bytecode node retains + // its own edge for the canonical constant. + let previous = std::mem::replace( + &mut linked_constants[index], + BytecodeConstant::Value(RawValue::String(canonical)), + ); + if let BytecodeConstant::Value(previous) = previous { + self.release_converted_value_edge(&previous); + } } property_key_atoms = bytecode_publish::link_constant_property_keys( &mut state, @@ -152,6 +168,13 @@ impl Runtime { &linked_constants, &mut auxiliary_atoms, )?; + let private_binding_publication = + bytecode_publish::prepare_private_binding_publication( + &local_definitions, + &closure_variables, + &linked_constants, + &state.heap, + )?; if let Some(debug) = unlinked_debug.take() { let filename = state.atoms.intern_property_key_js_string(&debug.filename)?; @@ -178,7 +201,8 @@ impl Runtime { .ok_or(RuntimeError::Invariant( "verified closure name was not a string constant", ))?; - let atom = state.atoms.intern_property_key_js_string(name)?; + let text = state.heap.string(*name)?.clone(); + let atom = state.atoms.intern_property_key_js_string(&text)?; auxiliary_atoms.push(atom); descriptor.name = ClosureVariableName::Atom(atom); } @@ -222,6 +246,7 @@ impl Runtime { Ok(()) })(); if let Err(error) = linking { + release_constant_edges(self, &linked_constants); state.release_atoms(auxiliary_atoms.drain(..))?; return Err(error); } @@ -247,9 +272,28 @@ impl Runtime { debug: linked_debug, auxiliary_atoms: auxiliary_atoms.into_boxed_slice(), }; + let producer_values = bytecode + .constants + .iter() + .filter_map(|constant| match constant { + BytecodeConstant::Value(raw) => Some(raw.clone()), + _ => None, + }) + .collect::>(); match state.heap.allocate_function_bytecode(bytecode) { - Ok(id) => id, + Ok(id) => { + // The bytecode node retained its own copy of every + // constant edge; release the caller-owned producer + // edges carried by the boundary conversion. + for raw in &producer_values { + self.release_converted_value_edge(raw); + } + id + } Err(error) => { + for raw in &producer_values { + self.release_converted_value_edge(raw); + } state.release_atoms(owned_atoms)?; return Err(error.into()); } @@ -409,7 +453,10 @@ impl Runtime { } pub(crate) enum FlatConstant { - Value(RawValue), + /// Unlinked primitive payload. String and BigInt literals stay as public + /// values here; the publish transaction below is their node creation + /// point (§2.2), where they enter the constant pool as `RawValue`s. + Value(Value), AtomString(JsString), RegExp { pattern: JsString, @@ -422,6 +469,17 @@ pub(crate) enum FlatConstant { Child(usize), } +/// Release every caller-owned string/BigInt producer edge carried by +/// converted value constants. Idempotent for constants without a node edge. +fn release_constant_edges(runtime: &Runtime, constants: &[BytecodeConstant]) { + for raw in constants.iter().filter_map(|constant| match constant { + BytecodeConstant::Value(raw) => Some(raw), + _ => None, + }) { + runtime.release_converted_value_edge(raw); + } +} + pub(crate) struct FlatFunction { pub(crate) code: Vec, pub(crate) constants: Vec, diff --git a/src/engine/heap/allocation.rs b/src/engine/heap/allocation.rs index 7f9c28e9..1c17710d 100644 --- a/src/engine/heap/allocation.rs +++ b/src/engine/heap/allocation.rs @@ -129,6 +129,34 @@ impl Heap { Ok(id) } + /// Allocate and publish a string node owning one `JsString` payload. + /// + /// The caller owns one returned string reference and must eventually call + /// [`Heap::release_string`]. String nodes have no outgoing heap edges, so + /// publication cannot fail after the slot is reserved. + pub fn allocate_string(&mut self, value: JsString) -> Result { + let (index, generation) = self.reserve(HeapNodeKind::String)?; + if let Err(error) = self.publish(index, NodeData::String(value)) { + self.abort_initializing(index)?; + return Err(error); + } + Ok(StringId { index, generation }) + } + + /// Allocate and publish a BigInt node owning one `JsBigInt` payload. + /// + /// The caller owns one returned BigInt reference and must eventually call + /// [`Heap::release_bigint`]. BigInt nodes have no outgoing heap edges, so + /// publication cannot fail after the slot is reserved. + pub fn allocate_bigint(&mut self, value: JsBigInt) -> Result { + let (index, generation) = self.reserve(HeapNodeKind::BigInt)?; + if let Err(error) = self.publish(index, NodeData::BigInt(value)) { + self.abort_initializing(index)?; + return Err(error); + } + Ok(BigIntId { index, generation }) + } + /// Allocate and publish a realm/context node, retaining all realm roots. /// Symbol atoms in `intrinsics` transfer to the node on success. pub fn allocate_context(&mut self, context: ContextData) -> Result { diff --git a/src/engine/heap/arena.rs b/src/engine/heap/arena.rs index c7452a57..85ec5726 100644 --- a/src/engine/heap/arena.rs +++ b/src/engine/heap/arena.rs @@ -1,4 +1,5 @@ use super::*; +use std::cell::Cell; impl Heap { #[must_use] @@ -149,7 +150,10 @@ impl Heap { "initializing slot metadata did not match its payload", )); } - slot.state = SlotState::Live(Node { strong, data }); + slot.state = SlotState::Live(Node { + strong: Cell::new(strong), + data, + }); Ok(()) } @@ -174,6 +178,38 @@ impl Heap { } } + /// Trusted accessor for a handle that a live owning edge keeps valid. + /// + /// The generation check runs only in debug builds; release builds keep the + /// `Vec` bounds check. A non-live slot or wrong kind at a trusted call site + /// is a heap invariant violation, so it panics rather than returning an + /// error. General and untrusted callers must keep using + /// [`Heap::live_node`]. + #[inline] + pub(in crate::engine::heap) fn live_node_fast(&self, id: RawId) -> &Node { + debug_assert!( + self.validate_slot_identity(id).is_ok(), + "trusted handle failed its debug identity check" + ); + match &self.slots[id.index() as usize].state { + SlotState::Live(node) => node, + _ => unreachable!("trusted handle reached a non-live slot"), + } + } + + /// Trusted mutable accessor paired with [`Heap::live_node_fast`]. + #[inline] + pub(in crate::engine::heap) fn live_node_fast_mut(&mut self, id: RawId) -> &mut Node { + debug_assert!( + self.validate_slot_identity(id).is_ok(), + "trusted handle failed its debug identity check" + ); + match &mut self.slots[id.index() as usize].state { + SlotState::Live(node) => node, + _ => unreachable!("trusted handle reached a non-live slot"), + } + } + pub(in crate::engine::heap) fn live_node_mut( &mut self, id: RawId, @@ -197,7 +233,9 @@ impl Heap { NodeData::Shape(_) | NodeData::VarRef(_) | NodeData::Context(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed object lookup reached another node payload", )), } @@ -212,7 +250,9 @@ impl Heap { NodeData::Object(_) | NodeData::Shape(_) | NodeData::Context(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed var-ref lookup reached another node payload", )), } diff --git a/src/engine/heap/binding_records.rs b/src/engine/heap/binding_records.rs index 74c6f49b..d78ca7d7 100644 --- a/src/engine/heap/binding_records.rs +++ b/src/engine/heap/binding_records.rs @@ -2,7 +2,7 @@ use super::*; /// Non-owning unresolved-global location. Every use checks realm, atom and the /// generational shape identity/revision; it never retains an old property value. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub(super) struct GlobalLocation { pub realm: ContextId, pub atom: crate::engine::atom::Atom, @@ -19,7 +19,7 @@ pub(super) struct GlobalLocation { /// current value. The root returned by [`Heap::allocate_var_ref`] is intended /// to be the active frame's ownership. Function-object closure slots retain /// the same identity and therefore keep the cell alive after frame teardown. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct VarRefData { pub value: RawValue, diff --git a/src/engine/heap/binding_storage.rs b/src/engine/heap/binding_storage.rs index 33133a89..1d84c1a2 100644 --- a/src/engine/heap/binding_storage.rs +++ b/src/engine/heap/binding_storage.rs @@ -9,12 +9,32 @@ impl Heap { NodeData::Object(_) | NodeData::Shape(_) | NodeData::Context(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed var-ref lookup reached another node payload", )), } } + /// Trusted shared read for a live `VarRefId` held by an owning root. + #[inline] + pub(in crate::engine::heap) fn var_ref_fast(&self, id: VarRefId) -> &VarRefData { + match &self.live_node_fast(RawId::VarRef(id)).data { + NodeData::VarRef(var_ref) => var_ref, + _ => unreachable!("trusted var-ref handle reached another node payload"), + } + } + + /// Trusted mutable read for a live `VarRefId` held by an owning root. + #[inline] + pub(in crate::engine::heap) fn var_ref_fast_mut(&mut self, id: VarRefId) -> &mut VarRefData { + match &mut self.live_node_fast_mut(RawId::VarRef(id)).data { + NodeData::VarRef(var_ref) => var_ref, + _ => unreachable!("trusted var-ref handle reached another node payload"), + } + } + /// Read immutable executable data without promoting any raw cpool edges. pub fn function_bytecode( &self, @@ -25,7 +45,9 @@ impl Heap { NodeData::Object(_) | NodeData::Shape(_) | NodeData::VarRef(_) - | NodeData::Context(_) => Err(HeapError::Invariant( + | NodeData::Context(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed bytecode lookup reached another node payload", )), } @@ -87,21 +109,23 @@ impl Heap { if !self.zero_queue.is_empty() || !immediate(&replacement) { return false; } - let Ok(cell) = self.var_ref_mut(id) else { - return false; - }; + // The caller holds an owning VarRef root, so the cell is live; a stale + // id is a heap invariant violation at this trusted boundary. The + // replacement is an immediate, so the only `validate_var_ref_value` + // rejection still reachable here is a module-import view, checked + // explicitly instead of running the full validator on every write. + let cell = self.var_ref_fast_mut(id); if cell.is_const || cell.kind.is_private() + || cell.kind == ClosureVariableKind::ModuleImportView || !immediate(&cell.value) || expected .is_some_and(|metadata| metadata != (cell.is_lexical, cell.is_const, cell.kind)) - || validate_var_ref_value(cell.kind, cell.is_lexical, cell.is_const, &replacement) - .is_err() { return false; } - // Same validator as replace_var_ref_value. Both edge sets and atom - // cleanup are empty, and the zero queue was empty before mutation. + // Both edge sets and atom cleanup are empty, and the zero queue was + // empty before mutation. cell.value = replacement; true } @@ -133,7 +157,7 @@ mod immediate_write_tests { let runtime = crate::engine::api::runtime::Runtime::new(); let root = runtime .new_var_ref( - crate::engine::value::Value::Int(1), + crate::engine::value::JsValue::Int(1), false, false, ClosureVariableKind::Normal, @@ -151,10 +175,10 @@ mod immediate_write_tests { .try_replace_immediate_var_ref_value(root.id(), RawValue::Int(2), None) ); assert_eq!(state.heap.zero_queue.len(), 1); - assert_eq!( + assert!(matches!( state.heap.var_ref(root.id()).unwrap().value, RawValue::Int(1) - ); + )); let cleanup = state.heap.drain_zero_queue().unwrap(); state.apply_cleanup(cleanup).unwrap(); assert!( @@ -162,9 +186,9 @@ mod immediate_write_tests { .heap .try_replace_immediate_var_ref_value(root.id(), RawValue::Int(2), None) ); - assert_eq!( + assert!(matches!( state.heap.var_ref(root.id()).unwrap().value, RawValue::Int(2) - ); + )); } } diff --git a/src/engine/heap/code_records.rs b/src/engine/heap/code_records.rs index 29e29b2a..7c6e1d79 100644 --- a/src/engine/heap/code_records.rs +++ b/src/engine/heap/code_records.rs @@ -1,7 +1,7 @@ use super::*; /// Constant-pool entry owned by a [`FunctionBytecodeData`] node. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum BytecodeConstant { Value(RawValue), /// Compile-once RegExp literal payload. These reference-counted Rust @@ -21,7 +21,7 @@ pub enum BytecodeConstant { /// layer which still owns both the exact source spelling and its interned /// [`Atom`], so it seals that distinction here before handing linked bytecode /// to the atom-table-independent heap. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub(crate) enum PublishedPrivateBindingRole { Primary, SetterStorage, @@ -32,7 +32,7 @@ pub(crate) enum PublishedPrivateBindingRole { /// or closure descriptor. Local setter halves also carry reciprocal `pair` /// indices; closure captures may legitimately retain only one half and leave /// it absent. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash)] pub(crate) struct PublishedPrivateBinding { pub(in crate::engine::heap) name: Atom, pub(in crate::engine::heap) role: PublishedPrivateBindingRole, @@ -151,7 +151,7 @@ pub struct FunctionBytecodeData { /// /// `filename` is backed by one distinct reference in `auxiliary_atoms`; it is /// intentionally not released separately when the bytecode node dies. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct FunctionDebugInfo { pub filename: Atom, pub pc2line: Option, diff --git a/src/engine/heap/collection_index.rs b/src/engine/heap/collection_index.rs index 4bf9cac4..a70f3f38 100644 --- a/src/engine/heap/collection_index.rs +++ b/src/engine/heap/collection_index.rs @@ -1,24 +1,28 @@ //! Non-owning key lookup for insertion-ordered strong collections. //! //! Records own keys and GC edges; this index owns only hashes and record IDs. -//! The collection storage boundary maintains both together. Lookups borrow records -//! without rooting every candidate or invoking JavaScript. Public iterator cursors remain the responsibility of ordered record storage. +//! The collection storage boundary maintains both together. Lookups borrow +//! records without rooting every candidate or invoking JavaScript. Public +//! iterator cursors remain the responsibility of ordered record storage. +//! +//! The string-hash memo is keyed by generational `StringId`: a live record +//! owns its key node, so the identity cannot be reclaimed or aliased while +//! cached, and slot reuse always changes the generation. -use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::fmt; use std::hash::{BuildHasher, Hasher}; -use super::{CollectionRecords, HeapError, RawValue}; -use crate::engine::value::{JsString, WeakJsString, collection_key}; +use super::{CollectionRecords, Heap, HeapError, RawValue, StringId}; +use crate::engine::value::collection_key; #[derive(Clone, Default)] pub struct CollectionIndex { buckets: HashMap, crate::engine::hash::IdentityBuildHasher>, key_hasher: std::collections::hash_map::RandomState, - // Bounded weak representation cache, scoped to the key hasher seed. - // Short and long string keys share it without keeping payloads alive. - string_hashes: RefCell>>, + // Bounded hash memo keyed by generational string identity, scoped to the + // key hasher seed. Non-owning: record keys keep their nodes alive. + string_hashes: std::cell::RefCell>>, #[cfg(test)] hash_computations: std::cell::Cell, } @@ -27,7 +31,7 @@ const STRING_HASH_CACHE_SIZE: usize = 8; #[derive(Clone)] struct CachedStringHash { - string: WeakJsString, + id: StringId, hash: u64, } @@ -37,21 +41,18 @@ struct StringHashCache { } impl StringHashCache { - fn find(&self, string: &JsString) -> Option { + fn find(&self, id: StringId) -> Option { self.entries .iter() - .find(|entry| entry.string.same_representation(string)) + .find(|entry| entry.id == id) .map(|entry| entry.hash) } - fn remember(&mut self, string: &JsString, hash: u64) { + fn remember(&mut self, id: StringId, hash: u64) { if self.entries.len() == STRING_HASH_CACHE_SIZE { self.entries.pop_front(); } - self.entries.push_back(CachedStringHash { - string: string.downgrade(), - hash, - }); + self.entries.push_back(CachedStringHash { id, hash }); } } @@ -73,56 +74,69 @@ impl fmt::Debug for CollectionIndex { } impl CollectionIndex { - fn hash(&self, key: &RawValue) -> u64 { - let RawValue::String(string) = key else { - return self.hash_uncached(key); + pub(super) fn hash(&self, heap: &Heap, key: &RawValue) -> u64 { + let RawValue::String(id) = key else { + return self.hash_uncached(heap, key); }; if let Some(hash) = self .string_hashes .borrow() .as_ref() - .and_then(|cache| cache.find(string)) + .and_then(|cache| cache.find(*id)) { return hash; } - let hash = self.hash_uncached(key); + let hash = self.hash_uncached(heap, key); self.string_hashes .borrow_mut() .get_or_insert_with(Box::default) - .remember(string, hash); + .remember(*id, hash); hash } - fn hash_uncached(&self, key: &RawValue) -> u64 { + fn hash_uncached(&self, heap: &Heap, key: &RawValue) -> u64 { #[cfg(test)] self.hash_computations.set(self.hash_computations.get() + 1); let mut hasher = self.key_hasher.build_hasher(); - collection_key::hash(key, &mut hasher); + collection_key::hash(heap, key, &mut hasher); hasher.finish() } - pub(super) fn find(&self, records: &CollectionRecords, key: &RawValue) -> Option { + pub(super) fn find( + &self, + heap: &Heap, + records: &CollectionRecords, + key: &RawValue, + ) -> Option { self.buckets - .get(&self.hash(key))? + .get(&self.hash(heap, key))? .iter() .copied() .find(|&index| { collection_key::same_value_zero( + heap, &records.get(index).expect("indexed record exists").key, key, ) }) } - pub(super) fn insert(&mut self, key: &RawValue, index: usize) -> u64 { - let hash = self.hash(key); - self.buckets.entry(hash).or_default().push(index); + #[cfg_attr(not(test), allow(dead_code))] + pub(super) fn insert(&mut self, heap: &Heap, key: &RawValue, index: usize) -> u64 { + let hash = self.hash(heap, key); + self.insert_hashed(hash, index); hash } + /// Index one record under a hash computed before the caller borrowed the + /// record set mutably. + pub(super) fn insert_hashed(&mut self, hash: u64, index: usize) { + self.buckets.entry(hash).or_default().push(index); + } + #[cfg(test)] - pub(super) fn remove(&mut self, key: &RawValue, index: usize) { - self.remove_hashed(self.hash(key), index); + pub(super) fn remove(&mut self, heap: &Heap, key: &RawValue, index: usize) { + self.remove_hashed(self.hash(heap, key), index); } pub(super) fn remove_hashed(&mut self, hash: u64, index: usize) { @@ -161,7 +175,11 @@ impl CollectionIndex { } /// Publication validation; never run this full scan on an ordinary lookup. - pub(super) fn validate(&self, records: &CollectionRecords) -> Result<(), HeapError> { + pub(super) fn validate( + &self, + heap: &Heap, + records: &CollectionRecords, + ) -> Result<(), HeapError> { let mut seen = std::collections::HashSet::new(); for (&hash, indices) in &self.buckets { if indices.is_empty() { @@ -175,7 +193,7 @@ impl CollectionIndex { .ok_or(HeapError::Invariant( "collection index points outside live records", ))?; - if !seen.insert(index) || self.hash_uncached(key) != hash { + if !seen.insert(index) || self.hash_uncached(heap, key) != hash { return Err(HeapError::Invariant( "collection index does not match its records", )); @@ -196,53 +214,77 @@ mod tests { use super::*; use crate::engine::heap::MapRecord; - fn record_store(values: Vec) -> CollectionRecords { + /// Build records around string handles allocated in the heap, the way the + /// storage boundary does. + fn string_key(heap: &mut Heap, text: &str) -> RawValue { + RawValue::String( + heap.allocate_string(crate::engine::value::JsString::try_from_utf8(text).unwrap()) + .unwrap(), + ) + } + + fn record_store(heap: &Heap, values: Vec) -> CollectionRecords { let mut records = CollectionRecords::default(); - for record in values { - records.insert(record); + for key in values { + let id = records.next_id(); + records.insert( + heap, + MapRecord { + key, + value: RawValue::Undefined, + }, + ); + assert_eq!(id + 1, records.next_id()); } records } - use crate::engine::value::{JsString, bigint::JsBigInt}; #[test] - fn short_string_hash_is_memoized_without_owning_the_key() { + fn short_string_hash_is_memoized_by_node_identity() { + let mut heap = Heap::new(); + let key = string_key(&mut heap, "short"); + let RawValue::String(id) = key else { + panic!("string key expected"); + }; let index = CollectionIndex::default(); - let key = JsString::try_from_utf8("short").unwrap(); - let weak = key.downgrade(); for _ in 0..3 { - index.hash(&RawValue::String(key.clone())); + index.hash(&heap, &key); } assert_eq!(index.hash_computations.get(), 1); - drop(key); - assert!(weak.upgrade().is_none()); + // Equal content in a different node is a distinct identity and hashes + // independently, but content equality still maps to the same bucket. + let alias = string_key(&mut heap, "short"); + assert_ne!( + match alias { + RawValue::String(alias) => alias, + _ => unreachable!(), + }, + id + ); + assert_eq!(index.hash(&heap, &key), index.hash(&heap, &alias)); + assert_eq!(index.hash_computations.get(), 2); } #[test] - fn long_string_hash_memo_is_bounded_and_does_not_own_keys() { + fn long_string_hash_memo_is_bounded() { + let mut heap = Heap::new(); + let key = string_key(&mut heap, &"x".repeat(1024)); let index = CollectionIndex::default(); - let key = JsString::try_from_utf8(&"x".repeat(1024)).unwrap(); - let weak = key.downgrade(); - let expected = index.hash(&RawValue::String(key.clone())); + let expected = index.hash(&heap, &key); for _ in 0..32 { - assert_eq!(index.hash(&RawValue::String(key.clone())), expected); + assert_eq!(index.hash(&heap, &key), expected); } assert_eq!(index.hash_computations.get(), 1); let cloned = index.clone(); - assert_eq!(cloned.hash(&RawValue::String(key.clone())), expected); + assert_eq!(cloned.hash(&heap, &key), expected); let independent = CollectionIndex::default(); assert_eq!( - independent.hash(&RawValue::String(key.clone())), - independent.hash_uncached(&RawValue::String(key.clone())) - ); - drop(key); - assert!( - weak.upgrade().is_none(), - "hash memo must not retain a String payload" + independent.hash(&heap, &key), + independent.hash_uncached(&heap, &key) ); for suffix in 0..32 { - let key = JsString::try_from_utf8(&("x".repeat(1024) + &suffix.to_string())).unwrap(); - index.hash(&RawValue::String(key)); + let key = string_key(&mut heap, &("x".repeat(1024) + &suffix.to_string())); + index.hash(&heap, &key); } assert!( index.string_hashes.borrow().as_ref().unwrap().entries.len() <= STRING_HASH_CACHE_SIZE @@ -251,7 +293,42 @@ mod tests { #[test] fn equal_keys_share_hash_across_number_and_string_representations() { + let mut heap = Heap::new(); let index = CollectionIndex::default(); + let string_left = string_key(&mut heap, "abc"); + let string_right = string_key(&mut heap, "abc"); + let bigint_left = RawValue::BigInt( + heap.allocate_bigint( + crate::engine::value::bigint::JsBigInt::parse_radix( + "123456789012345678901234567890", + 10, + ) + .unwrap(), + ) + .unwrap(), + ); + let bigint_right = RawValue::BigInt( + heap.allocate_bigint( + crate::engine::value::bigint::JsBigInt::parse_radix( + "123456789012345678901234567890", + 10, + ) + .unwrap(), + ) + .unwrap(), + ); + // Cross-kind comparisons reject before any pair moves into the table. + let one_string = string_key(&mut heap, "1"); + assert!(!collection_key::same_value_zero( + &heap, + &RawValue::Int(1), + &one_string + )); + assert!(!collection_key::same_value_zero( + &heap, + &RawValue::Int(1), + &bigint_left + )); let pairs = [ (RawValue::Int(0), RawValue::Float(-0.0)), (RawValue::Int(42), RawValue::Float(42.0)), @@ -259,67 +336,39 @@ mod tests { RawValue::Float(f64::NAN), RawValue::Float(f64::from_bits(0x7ff8_0000_0000_0042)), ), - ( - RawValue::String(JsString::from_static("abc")), - RawValue::String(JsString::try_from_utf16([97, 98, 99]).unwrap()), - ), - ( - RawValue::BigInt( - JsBigInt::parse_radix("123456789012345678901234567890", 10).unwrap(), - ), - RawValue::BigInt( - JsBigInt::parse_radix("123456789012345678901234567890", 10).unwrap(), - ), - ), + (string_left, string_right), + (bigint_left, bigint_right), ]; for (left, right) in pairs { - assert!(collection_key::same_value_zero(&left, &right)); - assert_eq!(index.hash(&left), index.hash(&right)); + assert!(collection_key::same_value_zero(&heap, &left, &right)); + assert_eq!(index.hash(&heap, &left), index.hash(&heap, &right)); } - assert!(!collection_key::same_value_zero( - &RawValue::Int(1), - &RawValue::String(JsString::from_static("1")) - )); - assert!(!collection_key::same_value_zero( - &RawValue::Int(1), - &RawValue::BigInt(JsBigInt::one()) - )); } #[test] fn collision_candidates_are_compared_and_removal_preserves_the_others() { + let mut heap = Heap::new(); let mut index = CollectionIndex::default(); - let records = record_store(vec![ - MapRecord { - key: RawValue::Int(1), - value: RawValue::Undefined, - }, - MapRecord { - key: RawValue::Int(2), - value: RawValue::Undefined, - }, - ]); + let records = record_store(&heap, vec![RawValue::Int(1), RawValue::Int(2)]); // Force a collision to exercise the bucket path deterministically, // without relying on the randomized hasher finding one naturally. - let hash = index.hash(&RawValue::Int(2)); + let hash = index.hash(&heap, &RawValue::Int(2)); index.buckets.insert(hash, vec![0, 1]); - assert_eq!(index.find(&records, &RawValue::Int(2)), Some(1)); - index.remove(&RawValue::Int(2), 1); - assert_eq!(index.find(&records, &RawValue::Int(2)), None); + assert_eq!(index.find(&heap, &records, &RawValue::Int(2)), Some(1)); + index.remove(&heap, &RawValue::Int(2), 1); + assert_eq!(index.find(&heap, &records, &RawValue::Int(2)), None); assert_eq!(index.buckets[&hash], vec![0]); } #[test] fn publication_rejects_missing_or_stale_index_entries() { + let mut heap = Heap::new(); let mut index = CollectionIndex::default(); - let mut records = record_store(vec![MapRecord { - key: RawValue::Int(1), - value: RawValue::Undefined, - }]); - assert!(index.validate(&records).is_err()); - index.insert(&RawValue::Int(1), 0); - assert!(index.validate(&records).is_ok()); + let mut records = record_store(&heap, vec![RawValue::Int(1)]); + assert!(index.validate(&heap, &records).is_err()); + index.insert(&heap, &RawValue::Int(1), 0); + assert!(index.validate(&heap, &records).is_ok()); records.get_mut(0).unwrap().key = RawValue::Int(2); - assert!(index.validate(&records).is_err()); + assert!(index.validate(&heap, &records).is_err()); } } diff --git a/src/engine/heap/collection_records.rs b/src/engine/heap/collection_records.rs index f3c339fd..695f01ac 100644 --- a/src/engine/heap/collection_records.rs +++ b/src/engine/heap/collection_records.rs @@ -9,15 +9,15 @@ use std::collections::{BTreeSet, HashMap, btree_set}; use super::collection_index::CollectionIndex; -use super::{HeapError, RawValue}; +use super::{Heap, HeapError, RawValue}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct MapRecord { pub key: RawValue, pub value: RawValue, } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct CollectionRecords { entries: HashMap, order: BTreeSet, @@ -69,8 +69,8 @@ impl CollectionRecords { Some((id, &self.entries[&id].0)) } - pub(super) fn find(&self, key: &RawValue) -> Option { - self.key_index.find(self, key) + pub(super) fn find(&self, heap: &Heap, key: &RawValue) -> Option { + self.key_index.find(heap, self, key) } /// Must run before retaining a new record's heap edges. @@ -81,14 +81,28 @@ impl CollectionRecords { Ok(()) } + /// Compute the storage key hash before the caller borrows this record set + /// mutably; hashing string and BigInt keys needs heap access. + pub(super) fn precompute_insert_hash(&self, heap: &Heap, key: &RawValue) -> u64 { + self.key_index.hash(heap, key) + } + /// The heap has validated the key, uniqueness and ID capacity before commit. - pub(super) fn insert(&mut self, record: MapRecord) -> usize { + #[cfg_attr(not(test), allow(dead_code))] + pub(super) fn insert(&mut self, heap: &Heap, record: MapRecord) -> usize { + let hash = self.precompute_insert_hash(heap, &record.key); + self.insert_hashed(record, hash) + } + + /// Commit a record whose storage hash was computed before the caller + /// borrowed this record set mutably (hashing string and BigInt keys + /// needs heap access). + pub(super) fn insert_hashed(&mut self, record: MapRecord, hash: u64) -> usize { let id = self.next_id; self.next_id = id .checked_add(1) .expect("collection insertion was preflighted"); - let key = &record.key; - let hash = self.key_index.insert(key, id); + self.key_index.insert_hashed(hash, id); assert!( self.entries.insert(id, (record, hash)).is_none(), "collection record ID was reused" @@ -123,7 +137,7 @@ impl CollectionRecords { } } - pub(super) fn validate(&self) -> Result<(), HeapError> { + pub(super) fn validate(&self, heap: &Heap) -> Result<(), HeapError> { if self.entries.len() != self.order.len() || self .order @@ -134,7 +148,7 @@ impl CollectionRecords { "collection record IDs do not match live storage", )); } - self.key_index.validate(self) + self.key_index.validate(heap, self) } } @@ -177,10 +191,11 @@ mod tests { #[test] fn collection_record_storage_reclaims_capacity_and_keeps_id_clock() { + let heap = Heap::new(); for peak in [64, 1024, 16384] { let mut records = CollectionRecords::default(); for key in 0..peak { - records.insert(record(key as i32, key as i32)); + records.insert(&heap, record(key as i32, key as i32)); } for id in 0..peak - 1 { records.remove(id).unwrap(); @@ -198,12 +213,13 @@ mod tests { assert_eq!(records.take_all().count(), 1); assert_eq!(records.entries.capacity(), 0); assert_eq!(records.key_index.retained_capacities(), (0, 0)); - assert_eq!(records.insert(record(1, 2)), peak); + assert_eq!(records.insert(&heap, record(1, 2)), peak); } } #[test] fn collection_record_storage_matches_a_tombstone_reference_model() { + let heap = Heap::new(); let mut records = CollectionRecords::default(); let mut model: Vec> = Vec::new(); let mut seed = 0x8cae_7753_u64; @@ -214,7 +230,7 @@ mod tests { let existing = model .iter() .position(|entry| entry.is_some_and(|(k, _)| k == key)); - assert_eq!(records.find(&RawValue::Int(key)), existing); + assert_eq!(records.find(&heap, &RawValue::Int(key)), existing); match (seed >> 16) % 7 { 0..=2 => { if let Some(id) = existing { @@ -222,7 +238,7 @@ mod tests { model[id] = Some((key, step)); } else { records.preflight_insert().unwrap(); - assert_eq!(records.insert(record(key, step)), model.len()); + assert_eq!(records.insert(&heap, record(key, step)), model.len()); model.push(Some((key, step))); } } @@ -251,7 +267,7 @@ mod tests { model.fill(None); } } - records.validate().unwrap(); + records.validate(&heap).unwrap(); assert_eq!(records.len(), model.iter().flatten().count()); assert_eq!(records.next_id(), model.len()); let actual = records @@ -263,13 +279,25 @@ mod tests { .flatten() .map(|&(key, value)| record(key, value)) .collect::>(); - assert_eq!( - actual, - expected - .iter() - .map(|entry| (&entry.key, &entry.value)) - .collect::>() - ); + assert_eq!(actual.len(), expected.len()); + for ((actual_key, actual_value), expected) in actual.iter().zip(expected.iter()) { + assert!( + crate::engine::value::collection_key::same_value_zero( + &heap, + actual_key, + &expected.key + ), + "key mismatch: {actual_key:?}" + ); + assert!( + crate::engine::value::collection_key::same_value_zero( + &heap, + actual_value, + &expected.value + ), + "value mismatch: {actual_value:?}" + ); + } } } diff --git a/src/engine/heap/collections.rs b/src/engine/heap/collections.rs index bf454de2..42ac7634 100644 --- a/src/engine/heap/collections.rs +++ b/src/engine/heap/collections.rs @@ -313,7 +313,7 @@ impl Heap { )); } match &self.object(id)?.payload { - ObjectPayload::Map { records } => Ok(records.find(key)), + ObjectPayload::Map { records } => Ok(records.find(self, key)), _ => Err(HeapError::Invariant( "Map lookup reached an object with the wrong class", )), @@ -355,8 +355,13 @@ impl Heap { "Map record contains an internal value sentinel", )); } - match &self.object(id)?.payload { - ObjectPayload::Map { records } => records.preflight_insert()?, + let hash = match &self.object(id)?.payload { + ObjectPayload::Map { records } => { + records.preflight_insert()?; + // Hash before the mutable payload borrow: string and BigInt + // keys resolve through the heap. + records.precompute_insert_hash(self, &key) + } _ => { return Err(HeapError::Invariant( "Map insertion reached an object with the wrong class", @@ -371,7 +376,7 @@ impl Heap { let ObjectPayload::Map { records } = &mut self.object_mut(id)?.payload else { unreachable!("Map payload was validated before retaining record edges") }; - records.insert(MapRecord { key, value }); + records.insert_hashed(MapRecord { key, value }, hash); Ok(HeapCleanup::default()) } @@ -620,7 +625,7 @@ impl Heap { )); } match &self.object(id)?.payload { - ObjectPayload::Set { records } => Ok(records.find(key)), + ObjectPayload::Set { records } => Ok(records.find(self, key)), _ => Err(HeapError::Invariant( "Set lookup reached an object with the wrong class", )), @@ -661,8 +666,11 @@ impl Heap { "Set record contains an internal value sentinel", )); } - match &self.object(id)?.payload { - ObjectPayload::Set { records } => records.preflight_insert()?, + let hash = match &self.object(id)?.payload { + ObjectPayload::Set { records } => { + records.preflight_insert()?; + records.precompute_insert_hash(self, &key) + } _ => { return Err(HeapError::Invariant( "Set insertion reached an object with the wrong class", @@ -676,10 +684,13 @@ impl Heap { let ObjectPayload::Set { records } = &mut self.object_mut(id)?.payload else { unreachable!("Set payload was validated before retaining record edges") }; - records.insert(MapRecord { - key, - value: RawValue::Undefined, - }); + records.insert_hashed( + MapRecord { + key, + value: RawValue::Undefined, + }, + hash, + ); Ok(HeapCleanup::default()) } diff --git a/src/engine/heap/dictionary_storage.rs b/src/engine/heap/dictionary_storage.rs index c8244d08..79514d57 100644 --- a/src/engine/heap/dictionary_storage.rs +++ b/src/engine/heap/dictionary_storage.rs @@ -37,9 +37,11 @@ impl Heap { ) -> Result { let shape_id = self.exclusive_dictionary_shape(id)?; let shape = self.shape(shape_id)?; - let index = shape.find(atom).ok_or(HeapError::Invariant( - "dictionary deletion requires an existing property", - ))? as usize; + let index = shape + .find(AtomIdx::from_raw(atom.raw())) + .ok_or(HeapError::Invariant( + "dictionary deletion requires an existing property", + ))? as usize; if !shape.is_dictionary() || !shape.entries()[index].flags.configurable { return Err(HeapError::Invariant( "dictionary deletion requires configurable dictionary storage", @@ -48,7 +50,7 @@ impl Heap { self.invalidate_property_layout(id); self.shape_mut(shape_id)? - .remove_dictionary_property(atom) + .remove_dictionary_property(AtomIdx::from_raw(atom.raw())) .expect("dictionary key was validated before mutation"); let slots = &mut self.object_mut(id)?.slots; let previous = slots.swap_remove(index); @@ -57,7 +59,7 @@ impl Heap { slots.shrink_to(len.saturating_mul(2).saturating_add(8)); } let mut cleanup = HeapCleanup::default(); - cleanup.atoms.push(atom); + cleanup.atoms.push(AtomIdx::from_raw(atom.raw())); cleanup.atoms.extend(property_slot_atoms(&previous)); for edge in property_slot_edges(&previous) { self.release_raw_no_drain(edge)?; diff --git a/src/engine/heap/gc.rs b/src/engine/heap/gc.rs index d0d054a2..7c83b7e8 100644 --- a/src/engine/heap/gc.rs +++ b/src/engine/heap/gc.rs @@ -6,14 +6,14 @@ use super::Edges; use super::{ - AsyncGeneratorRequestData, Atom, AutoInitProperty, BytecodeConstant, ContextData, ContextId, - FinalizationRegistryEntry, FunctionBytecodeData, FunctionBytecodeId, GeneratorActivationData, - GeneratorFrameBinding, Hash, HashMap, Heap, HeapError, InternalCallableData, NativeErrorKind, - Node, NodeData, ObjectData, ObjectId, ObjectPayload, PrimitiveKind, PrimitiveObjectData, - PromiseCapabilityData, PromiseReaction, PropertySlot, RawId, RawModuleEvaluationState, - RawModuleLinkRealm, RawModuleNamespaceState, RawModuleRecord, RawModuleRecordBody, RawValue, - Shape, ShapeId, SlotState, TypedArrayElementKind, VarRefData, VarRefId, VecDeque, - WeakCollectionKey, is_map_storable_value, + AsyncGeneratorRequestData, AtomIdx, AutoInitProperty, BigIntId, BytecodeConstant, ContextData, + ContextId, FinalizationRegistryEntry, FunctionBytecodeData, FunctionBytecodeId, + GeneratorActivationData, GeneratorFrameBinding, Hash, HashMap, Heap, HeapError, + InternalCallableData, NativeErrorKind, Node, NodeData, ObjectData, ObjectId, ObjectPayload, + PrimitiveKind, PrimitiveObjectData, PromiseCapabilityData, PromiseReaction, PropertySlot, + RawId, RawModuleEvaluationState, RawModuleLinkRealm, RawModuleNamespaceState, RawModuleRecord, + RawModuleRecordBody, RawValue, Shape, ShapeId, SlotState, StringId, TypedArrayElementKind, + VarRefData, VarRefId, VecDeque, WeakCollectionKey, is_map_storable_value, }; /// Resources finalized by a release, mutation, or collection operation. @@ -24,10 +24,16 @@ pub struct HeapCleanup { pub finalized_var_refs: usize, pub finalized_contexts: usize, pub finalized_function_bytecodes: usize, + pub finalized_strings: usize, + pub finalized_bigints: usize, /// Finalized shape identities for O(1) weak-cache unlinking. pub finalized_shape_ids: Vec, /// Owned non-GC atom edges detached from shapes and symbol values. - pub atoms: Vec, + /// + /// These carry unbranded [`AtomIdx`] values: finalization runs inside the + /// arena without `AtomTable` access, and the runtime releases each index + /// through the table's validated index path when applying the cleanup. + pub atoms: Vec, } impl HeapCleanup { @@ -45,6 +51,12 @@ impl HeapCleanup { self.finalized_function_bytecodes = self .finalized_function_bytecodes .saturating_add(other.finalized_function_bytecodes); + self.finalized_strings = self + .finalized_strings + .saturating_add(other.finalized_strings); + self.finalized_bigints = self + .finalized_bigints + .saturating_add(other.finalized_bigints); self.finalized_shape_ids .append(&mut other.finalized_shape_ids); self.atoms.append(&mut other.atoms); @@ -69,8 +81,8 @@ pub struct GcStats { /// [`HeapCleanup::atoms`] for the caller to release after collection. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum WeakSymbolGcEvent { - IsLive(Atom), - Release(Atom), + IsLive(AtomIdx), + Release(AtomIdx), } /// Whether an internal collection performs QuickJS's ordered weak-object @@ -87,7 +99,7 @@ enum WeakObjectGcMode { /// Publication through [`FinalizationJobSink`] transfers exactly one owned /// callback, realm, and held-value root per record. A Runtime adapter must not /// retain them again when it adopts the record. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub(crate) struct PreparedFinalizationJob { pub(crate) realm: ContextId, pub(crate) callback: ObjectId, @@ -123,6 +135,12 @@ impl Heap { self.retain_raw(RawId::Object(id), 1) } + /// Trusted hot-path retain for a live object handle. + #[inline] + pub(crate) fn retain_object_fast(&self, id: ObjectId) { + self.retain_raw_fast(RawId::Object(id)); + } + /// Duplicate one externally owned shape reference. pub fn retain_shape(&mut self, id: ShapeId) -> Result<(), HeapError> { self.retain_raw(RawId::Shape(id), 1) @@ -143,6 +161,49 @@ impl Heap { self.retain_raw(RawId::FunctionBytecode(id), 1) } + /// Duplicate one externally owned string node reference. + pub fn retain_string(&mut self, id: StringId) -> Result<(), HeapError> { + self.retain_raw(RawId::String(id), 1) + } + + /// Duplicate one externally owned BigInt node reference. + pub fn retain_bigint(&mut self, id: BigIntId) -> Result<(), HeapError> { + self.retain_raw(RawId::BigInt(id), 1) + } + + /// Validated shared-borrow duplicate of one object reference. + pub(crate) fn retain_object_shared(&self, id: ObjectId) -> Result<(), HeapError> { + self.retain_raw_shared(RawId::Object(id)) + } + + /// Validated shared-borrow duplicate of one string node reference. + pub(crate) fn retain_string_shared(&self, id: StringId) -> Result<(), HeapError> { + self.retain_raw_shared(RawId::String(id)) + } + + /// Validated shared-borrow duplicate of one BigInt node reference. + pub(crate) fn retain_bigint_shared(&self, id: BigIntId) -> Result<(), HeapError> { + self.retain_raw_shared(RawId::BigInt(id)) + } + + /// Validated shared-borrow duplicate of one captured-variable cell. + pub(crate) fn retain_var_ref_shared(&self, id: VarRefId) -> Result<(), HeapError> { + self.retain_raw_shared(RawId::VarRef(id)) + } + + /// Validated shared-borrow duplicate of one context reference. + pub(crate) fn retain_context_shared(&self, id: ContextId) -> Result<(), HeapError> { + self.retain_raw_shared(RawId::Context(id)) + } + + /// Validated shared-borrow duplicate of one function-bytecode reference. + pub(crate) fn retain_function_bytecode_shared( + &self, + id: FunctionBytecodeId, + ) -> Result<(), HeapError> { + self.retain_raw_shared(RawId::FunctionBytecode(id)) + } + /// Release one object reference and iteratively drain zero-reference nodes. pub fn release_object(&mut self, id: ObjectId) -> Result { self.release_and_drain(RawId::Object(id)) @@ -175,6 +236,20 @@ impl Heap { self.release_and_drain(RawId::FunctionBytecode(id)) } + /// Release one string node reference and iteratively drain zero-reference + /// nodes. String payloads own no edges, so the cleanup carries counts + /// only. + pub fn release_string(&mut self, id: StringId) -> Result { + self.release_and_drain(RawId::String(id)) + } + + /// Release one BigInt node reference and iteratively drain zero-reference + /// nodes. BigInt payloads own no edges, so the cleanup carries counts + /// only. + pub fn release_bigint(&mut self, id: BigIntId) -> Result { + self.release_and_drain(RawId::BigInt(id)) + } + /// Snapshot the non-owning target currently stored by a genuine WeakRef. /// A stale identity remains visible here until the next weak-object pass; /// the runtime must still perform AtomTable liveness for Symbol targets. @@ -341,8 +416,8 @@ impl Heap { ) -> Result { for value in values { cleanup.atoms.extend(raw_value_atom(&value)); - if let RawValue::Object(object) = value { - self.release_raw_no_drain(RawId::Object(object))?; + for edge in raw_value_edges(&value) { + self.release_raw_no_drain(edge)?; } } cleanup.merge(self.drain_zero_queue()?); @@ -411,7 +486,7 @@ impl Heap { let mut examined_nodes = 0usize; for (index, slot) in self.slots.iter().enumerate() { if let SlotState::Live(node) = &slot.state { - trial[index] = Some(node.strong); + trial[index] = Some(node.strong.get()); examined_nodes = examined_nodes.saturating_add(1); } } @@ -489,6 +564,10 @@ impl Heap { generation: slot.generation, })), NodeData::Shape(_) | NodeData::VarRef(_) => {} + // String and BigInt payloads own no edges, so an unreachable + // one cannot participate in a cycle; it is reclaimed purely by + // its reference count through the zero queue. + NodeData::String(_) | NodeData::BigInt(_) => {} } } @@ -603,9 +682,9 @@ impl Heap { "ordered weak-map record disappeared during pruning", ))? }; - if let Some(atom) = raw_value_atom(&value) { - if !hook(WeakSymbolGcEvent::Release(atom))? { - cleanup.atoms.push(atom); + if let Some(index) = raw_value_atom(&value) { + if !hook(WeakSymbolGcEvent::Release(index))? { + cleanup.atoms.push(index); } } for edge in raw_value_edges(&value) { @@ -692,10 +771,10 @@ impl Heap { }; data.entries.remove(entry_index) }; - if let Some(atom) = raw_value_atom(&entry.held_value) - && !hook(WeakSymbolGcEvent::Release(atom))? + if let Some(index) = raw_value_atom(&entry.held_value) + && !hook(WeakSymbolGcEvent::Release(index))? { - cleanup.atoms.push(atom); + cleanup.atoms.push(index); } for edge in raw_value_edges(&entry.held_value) { self.release_raw_no_drain(edge)?; @@ -973,7 +1052,11 @@ impl Heap { { match target { WeakCollectionKey::Object(object) => Ok(self.is_live(RawId::Object(object))), - WeakCollectionKey::Symbol(atom) => hook(WeakSymbolGcEvent::IsLive(atom)), + // The key was branded at admission; the event carries the same + // table's unbranded index. + WeakCollectionKey::Symbol(atom) => { + hook(WeakSymbolGcEvent::IsLive(AtomIdx::from_raw(atom.raw()))) + } } } @@ -1022,6 +1105,7 @@ impl Heap { fn preflight_edge_retain(&self, edge: RawId, additional: u32) -> Result<(), HeapError> { self.live_node(edge)? .strong + .get() .checked_add(additional) .ok_or(HeapError::Overflow { operation: "retaining outgoing heap edges", @@ -1031,12 +1115,44 @@ impl Heap { pub(super) fn retain_raw(&mut self, id: RawId, additional: u32) -> Result<(), HeapError> { let node = self.live_node_mut(id)?; - node.strong = node - .strong - .checked_add(additional) - .ok_or(HeapError::Overflow { - operation: "retaining a heap reference", - })?; + node.strong.set( + node.strong + .get() + .checked_add(additional) + .ok_or(HeapError::Overflow { + operation: "retaining a heap reference", + })?, + ); + Ok(()) + } + + /// Trusted hot-path retain for a proven-live handle. + /// + /// Callers hold a live owning edge, so a stale or wrong-kind handle is a + /// heap invariant violation rather than a recoverable condition. The + /// count saturates at `u32::MAX`, matching QuickJS's immortal value; the + /// fallible [`Heap::retain_raw`] keeps its checked overflow behavior. + #[inline] + pub(in crate::engine::heap) fn retain_raw_fast(&self, id: RawId) { + let node = self.live_node_fast(id); + node.strong.set(node.strong.get().saturating_add(1)); + } + + /// Validated shared-borrow retain: full identity check, then the `Cell` + /// counter increment. Rooting paths (for example a nested property + /// materialization which already holds a shared state borrow) duplicate + /// one reference without requiring `&mut` access to the arena. + #[inline] + pub(in crate::engine::heap) fn retain_raw_shared(&self, id: RawId) -> Result<(), HeapError> { + let node = self.live_node(id)?; + node.strong.set( + node.strong + .get() + .checked_add(1) + .ok_or(HeapError::Overflow { + operation: "retaining a heap reference", + })?, + ); Ok(()) } @@ -1067,12 +1183,14 @@ impl Heap { let slot = &mut self.slots[index]; match &mut slot.state { SlotState::Live(node) => { - node.strong = node.strong.checked_sub(1).ok_or(HeapError::Underflow { - kind: id.kind(), - index: id.index(), - generation: id.generation(), - })?; - if node.strong == 0 { + node.strong.set(node.strong.get().checked_sub(1).ok_or( + HeapError::Underflow { + kind: id.kind(), + index: id.index(), + generation: id.generation(), + }, + )?); + if node.strong.get() == 0 { let state = std::mem::replace(&mut slot.state, SlotState::Vacant); let SlotState::Live(node) = state else { return Err(HeapError::Invariant( @@ -1127,7 +1245,7 @@ impl Heap { "zero queue referenced a node not in ZeroQueued state", )); }; - if node.strong != 0 { + if node.strong.get() != 0 { return Err(HeapError::Invariant( "zero queue contained a nonzero reference count", )); @@ -1208,6 +1326,14 @@ impl Heap { self.release_raw_no_drain(edge)?; } } + // String and BigInt payloads own no heap edges and no atom + // references; finalization only accounts for the node itself. + NodeData::String(_) => { + cleanup.finalized_strings = cleanup.finalized_strings.saturating_add(1); + } + NodeData::BigInt(_) => { + cleanup.finalized_bigints = cleanup.finalized_bigints.saturating_add(1); + } } Ok(()) } @@ -1242,7 +1368,7 @@ impl Heap { } slot.state = SlotState::Zombie { kind: id.kind(), - strong: node.strong, + strong: node.strong.get(), }; node }; @@ -1316,9 +1442,7 @@ pub(super) fn object_edges(object: &ObjectData) -> Edges { ObjectPayload::Array { dense } => { if let Some(dense) = dense { for value in dense { - if let RawValue::Object(object) = value { - edges.push(RawId::Object(*object)); - } + edges.extend(raw_value_edges(value)); } } } @@ -1651,8 +1775,19 @@ pub(super) fn property_slot_edges(slot: &PropertySlot) -> Edges { pub(super) fn raw_value_edges(value: &RawValue) -> Edges { let mut edges = Edges::new(); - if let RawValue::Object(object) = value { - edges.push(RawId::Object(*object)); + match value { + RawValue::Object(object) => edges.push(RawId::Object(*object)), + RawValue::String(id) => edges.push(RawId::String(*id)), + RawValue::BigInt(id) => edges.push(RawId::BigInt(*id)), + RawValue::Undefined + | RawValue::Null + | RawValue::Bool(_) + | RawValue::Int(_) + | RawValue::Float(_) + | RawValue::Symbol(_) + | RawValue::Private(_) + | RawValue::Uninitialized + | RawValue::Exception => {} } edges } @@ -1844,7 +1979,7 @@ pub(super) fn function_bytecode_edges(bytecode: &FunctionBytecodeData) -> Vec impl Iterator + '_ { +pub(super) fn property_slot_atoms(slot: &PropertySlot) -> impl Iterator + '_ { match slot { PropertySlot::Data(RawValue::Symbol(atom) | RawValue::Private(atom)) => Some(*atom), PropertySlot::Data(_) @@ -1855,11 +1990,11 @@ pub(super) fn property_slot_atoms(slot: &PropertySlot) -> impl Iterator impl Iterator + '_ { +fn object_slot_atoms(object: &ObjectData) -> impl Iterator + '_ { object.slots.iter().flat_map(property_slot_atoms) } -fn internal_callable_atoms(internal: &InternalCallableData) -> Vec { +fn internal_callable_atoms(internal: &InternalCallableData) -> Vec { match internal { InternalCallableData::PromiseCapabilityExecutor(capture) => capture .resolve @@ -1885,9 +2020,11 @@ fn internal_callable_atoms(internal: &InternalCallableData) -> Vec { } } -pub(super) fn object_atoms(object: &ObjectData) -> impl Iterator + '_ { +pub(super) fn object_atoms(object: &ObjectData) -> impl Iterator + '_ { let payload = match &object.payload { - ObjectPayload::Primitive(PrimitiveObjectData::Symbol(atom)) => vec![*atom], + ObjectPayload::Primitive(PrimitiveObjectData::Symbol(atom)) => { + vec![AtomIdx::from_raw(atom.raw())] + } ObjectPayload::Primitive( PrimitiveObjectData::Number(_) | PrimitiveObjectData::String(_) @@ -1995,12 +2132,14 @@ pub(super) fn object_atoms(object: &ObjectData) -> impl Iterator + | ObjectPayload::NativeFunction { .. } | ObjectPayload::BytecodeFunction { .. } => Vec::new(), }; - object_slot_atoms(object) - .chain(payload) - .chain(object.private_brand_home) + object_slot_atoms(object).chain(payload).chain( + object + .private_brand_home + .map(|atom| AtomIdx::from_raw(atom.raw())), + ) } -pub(super) fn generator_activation_atoms(activation: &GeneratorActivationData) -> Vec { +pub(super) fn generator_activation_atoms(activation: &GeneratorActivationData) -> Vec { let vm = &activation.vm; vm.stack .iter() @@ -2016,7 +2155,7 @@ pub(super) fn generator_activation_atoms(activation: &GeneratorActivationData) - .chain(activation.locals.iter()) .filter_map(|binding| match binding { GeneratorFrameBinding::Direct(value) => raw_value_atom(value), - GeneratorFrameBinding::Private(atom) => Some(*atom), + GeneratorFrameBinding::Private(atom) => Some(AtomIdx::from_raw(atom.raw())), GeneratorFrameBinding::PrivateCallable(_) | GeneratorFrameBinding::Uninitialized | GeneratorFrameBinding::Captured(_) => None, @@ -2025,9 +2164,9 @@ pub(super) fn generator_activation_atoms(activation: &GeneratorActivationData) - .collect() } -pub(super) fn raw_value_atom(value: &RawValue) -> Option { +pub(super) fn raw_value_atom(value: &RawValue) -> Option { match value { - RawValue::Symbol(atom) | RawValue::Private(atom) => Some(*atom), + RawValue::Symbol(index) | RawValue::Private(index) => Some(*index), RawValue::Undefined | RawValue::Null | RawValue::Bool(_) @@ -2046,12 +2185,16 @@ pub(super) fn raw_value_matches_weak_key(value: &RawValue, key: WeakCollectionKe (RawValue::Object(value), WeakCollectionKey::Object(key)) => { value.index == key.index && value.generation == key.generation } - (RawValue::Symbol(value), WeakCollectionKey::Symbol(key)) => *value == key, + // The weak key was branded at admission; the stored value carries the + // same table's unbranded index, so raw-index equality is exact. + (RawValue::Symbol(value), WeakCollectionKey::Symbol(key)) => { + *value == AtomIdx::from_raw(key.raw()) + } _ => false, } } -fn context_atoms(context: &ContextData) -> impl Iterator + '_ { +fn context_atoms(context: &ContextData) -> impl Iterator + '_ { context.intrinsics.iter().filter_map(raw_value_atom).chain( context .loaded_modules @@ -2062,7 +2205,9 @@ fn context_atoms(context: &ContextData) -> impl Iterator + '_ { ) } -pub(super) fn raw_module_record_atoms(record: &RawModuleRecord) -> impl Iterator + '_ { +pub(super) fn raw_module_record_atoms( + record: &RawModuleRecord, +) -> impl Iterator + '_ { let body = match &record.body { RawModuleRecordBody::Json { default_value } => raw_value_atom(default_value), RawModuleRecordBody::Parsing @@ -2080,11 +2225,11 @@ pub(super) fn raw_module_record_atoms(record: &RawModuleRecord) -> impl Iterator body.into_iter().chain(evaluation) } -fn function_bytecode_atoms(bytecode: &FunctionBytecodeData) -> impl Iterator + '_ { +fn function_bytecode_atoms(bytecode: &FunctionBytecodeData) -> impl Iterator + '_ { bytecode .auxiliary_atoms .iter() - .copied() + .map(|atom| AtomIdx::from_raw(atom.raw())) .chain( bytecode .constants @@ -2096,6 +2241,79 @@ fn function_bytecode_atoms(bytecode: &FunctionBytecodeData) -> impl Iterator impl Iterator + '_ { +fn var_ref_atoms(var_ref: &VarRefData) -> impl Iterator + '_ { raw_value_atom(&var_ref.value).into_iter() } + +#[cfg(debug_assertions)] +impl Heap { + /// Debug-only: list live nodes whose strong count exceeds internal + /// incoming edges, i.e. nodes retained by external roots. + pub(crate) fn debug_external_roots( + &self, + ) -> Vec<(crate::engine::heap::HeapNodeKind, usize, u32, String)> { + let mut incoming = vec![0usize; self.slots.len()]; + for slot in &self.slots { + if let SlotState::Live(node) = &slot.state { + for edge in node.data.edges() { + if let Ok(index) = self.live_index(edge) { + incoming[index] = incoming[index].saturating_add(1); + } + } + } + } + let mut bytecode_names = std::collections::HashMap::new(); + for (index, slot) in self.slots.iter().enumerate() { + if let SlotState::Live(node) = &slot.state { + if let NodeData::FunctionBytecode(data) = &node.data { + bytecode_names.insert( + index, + match &data.func_name { + Some(name) => name.to_utf8_lossy(), + None => "".to_string(), + }, + ); + } + } + } + let mut roots = Vec::new(); + for (index, slot) in self.slots.iter().enumerate() { + if let SlotState::Live(node) = &slot.state { + let strong = node.strong.get() as usize; + if strong > incoming[index] { + let detail = match &node.data { + NodeData::Object(object) => match &object.payload { + ObjectPayload::NativeFunction { data, .. } => { + let native = format!("{data:?}"); + format!("{:?}:{}", object.kind, &native[..native.len().min(120)]) + } + ObjectPayload::BytecodeFunction { bytecode, .. } => { + let name = bytecode_names + .get(&(bytecode.index as usize)) + .cloned() + .unwrap_or_default(); + format!("BytecodeFunction#{}:{name}", bytecode.index) + } + _ => format!("{:?}", object.kind), + }, + NodeData::FunctionBytecode(data) => match &data.func_name { + Some(name) => format!("bytecode:{}", name.to_utf8_lossy()), + None => "bytecode:".to_string(), + }, + NodeData::String(text) => { + text.to_utf8_lossy().chars().take(60).collect::() + } + _ => String::new(), + }; + roots.push(( + node.data.kind(), + index, + (strong - incoming[index]) as u32, + detail, + )); + } + } + } + roots + } +} diff --git a/src/engine/heap/identity.rs b/src/engine/heap/identity.rs index f128aebe..07d1105b 100644 --- a/src/engine/heap/identity.rs +++ b/src/engine/heap/identity.rs @@ -122,6 +122,65 @@ impl fmt::Debug for FunctionBytecodeId { } } +/// Stable identity of a string node until that slot is reclaimed. +/// +/// String nodes own one `Rc` payload (the public `JsString`) and +/// have no outgoing heap edges. A value slot holding a `StringId` owns one +/// edge to this node; reading the payload clones the inner `Rc`, so string +/// identity (`ptr_eq`) semantics survive arena indirection unchanged. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StringId { + pub(in crate::engine::heap) index: u32, + pub(in crate::engine::heap) generation: u32, +} + +impl StringId { + /// Arena index, intended for diagnostics and serialized debug traces only. + #[must_use] + #[cfg(test)] + pub const fn debug_index(self) -> u32 { + self.index + } + + /// Slot generation, intended for diagnostics and serialized debug traces. + #[must_use] + #[cfg(test)] + pub const fn debug_generation(self) -> u32 { + self.generation + } +} + +impl fmt::Debug for StringId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("StringId") + .field("index", &self.index) + .field("generation", &self.generation) + .finish() + } +} + +/// Stable identity of a BigInt node until that slot is reclaimed. +/// +/// BigInt nodes own one `JsBigInt` payload and have no outgoing heap edges. +/// As with strings, value slots holding a `BigIntId` own one edge, and reading +/// the payload hands out a clone of the payload itself. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BigIntId { + pub(in crate::engine::heap) index: u32, + pub(in crate::engine::heap) generation: u32, +} + +impl fmt::Debug for BigIntId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("BigIntId") + .field("index", &self.index) + .field("generation", &self.generation) + .finish() + } +} + /// Runtime heap node category. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum HeapNodeKind { @@ -130,6 +189,8 @@ pub enum HeapNodeKind { VarRef, Context, FunctionBytecode, + String, + BigInt, } /// Failure of a checked heap ownership operation. @@ -193,24 +254,30 @@ impl Error for HeapError {} /// Heap-internal value payload. /// -/// `Clone` duplicates raw payload bytes and primitive backing stores; it does -/// **not** retain an object edge. Owned clones may enter the heap only through -/// checked methods such as [`Heap::allocate_object`] and +/// `Clone` copies the raw payload and duplicates nothing: heap-backed kinds +/// are generational handles (string/BigInt nodes, unbranded atom indices, +/// object slots), so an owned clone enters the heap only through checked +/// methods such as [`Heap::allocate_object`] and /// [`Heap::replace_object_slot`], which retain their edges transactionally. -#[derive(Clone, Debug, PartialEq)] +/// +/// There is deliberately no `PartialEq`: handle equality is *not* content +/// equality for strings and BigInts. Key comparison goes through +/// `value::collection_key` (id fast path plus content fallback with heap +/// access); identity comparison uses explicit handle equality. +#[derive(Clone, Debug)] pub enum RawValue { Undefined, Null, Bool(bool), Int(i32), Float(f64), - BigInt(JsBigInt), - String(JsString), - Symbol(Atom), + BigInt(BigIntId), + String(StringId), + Symbol(AtomIdx), /// Heap-internal class-private identity. This owns one private-atom /// reference exactly like `Symbol`, but it is not an ECMAScript Value and /// must never cross `Runtime::root_raw_value` or enter ordinary storage. - Private(Atom), + Private(AtomIdx), Object(ObjectId), Uninitialized, #[cfg_attr( @@ -223,6 +290,35 @@ pub enum RawValue { Exception, } +const _: () = assert!(std::mem::size_of::() <= 16); + +impl RawValue { + /// The one producer-owned heap edge carried by a value produced at a + /// boundary conversion (`Runtime::raw_property_value` and friends): + /// a freshly allocated string or BigInt node. + /// + /// Transactional store paths retain their own edge for the stored copy, + /// so the caller releases this producer edge once the store has + /// succeeded (or immediately when the value is never stored). + #[must_use] + pub(crate) fn conversion_node_edge(&self) -> Option { + match self { + Self::String(id) => Some(RawId::String(*id)), + Self::BigInt(id) => Some(RawId::BigInt(*id)), + Self::Undefined + | Self::Null + | Self::Bool(_) + | Self::Int(_) + | Self::Float(_) + | Self::Symbol(_) + | Self::Private(_) + | Self::Object(_) + | Self::Uninitialized + | Self::Exception => None, + } + } +} + /// Append-only identity of one module record in a Context-owned loaded-module /// cache. A removed record leaves a tombstone and its identity is never /// reused, matching the construction-order identity of QuickJS's diff --git a/src/engine/heap/iteration_records.rs b/src/engine/heap/iteration_records.rs index a3054e51..43b7a166 100644 --- a/src/engine/heap/iteration_records.rs +++ b/src/engine/heap/iteration_records.rs @@ -6,7 +6,7 @@ use super::*; /// `held_value` owns its ordinary object or Symbol edge until the registration /// is unregistered, finalized with its registry, or moved into a prepared /// finalization job by the ordered weak-object pass. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(in crate::engine::heap) struct FinalizationRegistryEntry { pub(in crate::engine::heap) target: WeakCollectionKey, pub(in crate::engine::heap) held_value: RawValue, @@ -19,7 +19,7 @@ pub(in crate::engine::heap) struct FinalizationRegistryEntry { /// remain in registration order so token clearing, target clearing, and job /// preparation follow pinned QuickJS's single forward traversal. #[doc(hidden)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct FinalizationRegistryData { pub(in crate::engine::heap) callback: ObjectId, pub(in crate::engine::heap) realm: ContextId, @@ -28,14 +28,14 @@ pub struct FinalizationRegistryData { /// One string-key entry captured by QuickJS's `JS_GPN_SET_ENUM` enumeration. /// `JsString` avoids storing runtime-owning `PropertyKey` roots in the heap. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct ForInProperty { pub name: JsString, pub enumerable: bool, } /// Mutable state of one hidden for-in enumeration object. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct ForInIteratorData { pub object: Option, pub index: usize, @@ -51,7 +51,7 @@ pub struct ForInIteratorData { /// One non-observable step selected from a hidden for-in iterator. The /// runtime performs live property/prototype operations only after the heap /// borrow used to advance the cursor has ended. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub enum ForInCandidate { Done, BaseComplete { object: ObjectId, fast_array: bool }, diff --git a/src/engine/heap/iterator_records.rs b/src/engine/heap/iterator_records.rs index 5dac6f8f..187db0b9 100644 --- a/src/engine/heap/iterator_records.rs +++ b/src/engine/heap/iterator_records.rs @@ -5,7 +5,7 @@ use super::*; /// The eager consumers (`every`, `find`, `forEach`, and `some`) do not /// allocate a helper payload and therefore use [`IteratorConsumerKind`] /// instead. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum IteratorHelperKind { Drop, Filter, @@ -15,7 +15,7 @@ pub enum IteratorHelperKind { } /// Eager operation selected by the shared Iterator consumer implementation. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum IteratorConsumerKind { Every, Find, @@ -24,7 +24,7 @@ pub enum IteratorConsumerKind { } /// Resume operation shared by Iterator Helper and Iterator Wrap prototypes. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum IteratorResumeKind { Next, Return, @@ -36,7 +36,7 @@ pub enum IteratorResumeKind { /// raw arena-owned values because property lookup can produce any ECMAScript /// value. QuickJS keeps all four edges alive until finalization even after /// `done` becomes true. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct IteratorHelperData { pub source: ObjectId, pub next: RawValue, @@ -49,7 +49,7 @@ pub struct IteratorHelperData { } /// Hidden state of an Iterator created by `Iterator.from`. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct IteratorWrapData { pub source: RawValue, pub next: RawValue, @@ -60,7 +60,7 @@ pub struct IteratorWrapData { /// /// The source is known to be an object after `GetIterator`, while `next` /// remains an arbitrary cached ECMAScript value until the first call. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct AsyncFromSyncIteratorData { pub sync_iterator: ObjectId, pub next: RawValue, @@ -71,14 +71,14 @@ pub struct AsyncFromSyncIteratorData { /// Consumed slots become `None` so their edges can be released immediately, /// matching QuickJS's advancing finalizer boundary without shifting the /// remaining vector. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct IteratorConcatItem { pub iterable: ObjectId, pub method: RawValue, } /// Hidden state of the lazy iterator returned by `Iterator.concat`. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct IteratorConcatData { pub items: Vec>, pub index: usize, @@ -88,7 +88,7 @@ pub struct IteratorConcatData { } #[cfg(test)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub(in crate::engine::heap) enum IteratorHelperRawValueField { Next, Callback, @@ -158,10 +158,12 @@ pub(in crate::engine::heap) fn validate_iterator_wrap_data( .into_iter() .chain(raw_value_edges(&data.next)) { - let RawId::Object(object) = edge else { - unreachable!("RawValue only owns object edges") - }; - heap.object(object)?; + if !heap.is_live(edge) { + return Err(HeapError::Stale { + index: edge.index(), + generation: edge.generation(), + }); + } } Ok(()) } @@ -209,10 +211,12 @@ pub(in crate::engine::heap) fn validate_iterator_concat_data( )); } for edge in raw_value_edges(&data.next) { - let RawId::Object(object) = edge else { - unreachable!("RawValue only owns object edges") - }; - heap.object(object)?; + if !heap.is_live(edge) { + return Err(HeapError::Stale { + index: edge.index(), + generation: edge.generation(), + }); + } } Ok(()) } diff --git a/src/engine/heap/mod.rs b/src/engine/heap/mod.rs index 3628cee7..6d8d2f89 100644 --- a/src/engine/heap/mod.rs +++ b/src/engine/heap/mod.rs @@ -64,6 +64,7 @@ pub use collections::{WeakCollectionKey, WeakCollectionRecords}; mod private_validation; use crate::engine::api::error::NativeErrorKind; use crate::engine::atom::Atom; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native; use native::{ ArrayBufferNativeKind, ArrayIteratorKind, DataViewNativeKind, DynamicFunctionKind, @@ -116,6 +117,8 @@ pub struct HeapCounts { pub var_ref_nodes: usize, pub context_nodes: usize, pub function_bytecode_nodes: usize, + pub string_nodes: usize, + pub bigint_nodes: usize, pub initializing: usize, pub live: usize, pub zero_queued: usize, @@ -126,12 +129,14 @@ pub struct HeapCounts { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum RawId { +pub(crate) enum RawId { Object(ObjectId), Shape(ShapeId), VarRef(VarRefId), Context(ContextId), FunctionBytecode(FunctionBytecodeId), + String(StringId), + BigInt(BigIntId), } impl RawId { @@ -142,6 +147,8 @@ impl RawId { Self::VarRef(_) => HeapNodeKind::VarRef, Self::Context(_) => HeapNodeKind::Context, Self::FunctionBytecode(_) => HeapNodeKind::FunctionBytecode, + Self::String(_) => HeapNodeKind::String, + Self::BigInt(_) => HeapNodeKind::BigInt, } } @@ -152,6 +159,8 @@ impl RawId { Self::VarRef(id) => id.index, Self::Context(id) => id.index, Self::FunctionBytecode(id) => id.index, + Self::String(id) => id.index, + Self::BigInt(id) => id.index, } } @@ -162,6 +171,8 @@ impl RawId { Self::VarRef(id) => id.generation, Self::Context(id) => id.generation, Self::FunctionBytecode(id) => id.generation, + Self::String(id) => id.generation, + Self::BigInt(id) => id.generation, } } } @@ -173,6 +184,8 @@ enum NodeData { VarRef(VarRefData), Context(Box), FunctionBytecode(FunctionBytecodeData), + String(JsString), + BigInt(JsBigInt), } impl NodeData { @@ -183,6 +196,8 @@ impl NodeData { Self::VarRef(_) => HeapNodeKind::VarRef, Self::Context(_) => HeapNodeKind::Context, Self::FunctionBytecode(_) => HeapNodeKind::FunctionBytecode, + Self::String(_) => HeapNodeKind::String, + Self::BigInt(_) => HeapNodeKind::BigInt, } } @@ -193,12 +208,16 @@ impl NodeData { Self::VarRef(var_ref) => var_ref_edges(var_ref), Self::Context(context) => context_edges(context).into(), Self::FunctionBytecode(bytecode) => function_bytecode_edges(bytecode).into(), + // String and BigInt payloads keep their resource ownership inside + // the `Rc` payload (rope children stay in the rope tree) and own no + // heap edges, so cascade-only cycle handling holds trivially. + Self::String(_) | Self::BigInt(_) => Edges::new(), } } } struct Node { - strong: u32, + strong: Cell, data: NodeData, } @@ -223,7 +242,7 @@ impl SlotState { const fn strong(&self) -> Option { match self { Self::Initializing { strong, .. } | Self::Zombie { strong, .. } => Some(*strong), - Self::Live(node) | Self::ZeroQueued(node) => Some(node.strong), + Self::Live(node) | Self::ZeroQueued(node) => Some(node.strong.get()), Self::Vacant | Self::Retired => None, } } @@ -330,6 +349,8 @@ fn increment_kind_count(counts: &mut HeapCounts, kind: HeapNodeKind) { HeapNodeKind::FunctionBytecode => { counts.function_bytecode_nodes = counts.function_bytecode_nodes.saturating_add(1); } + HeapNodeKind::String => counts.string_nodes = counts.string_nodes.saturating_add(1), + HeapNodeKind::BigInt => counts.bigint_nodes = counts.bigint_nodes.saturating_add(1), } } @@ -395,6 +416,8 @@ mod realm_storage; mod dictionary_storage; mod object_storage; +mod value_storage; + mod binding_storage; mod module_storage; diff --git a/src/engine/heap/module_records.rs b/src/engine/heap/module_records.rs index 0b399025..254b36e2 100644 --- a/src/engine/heap/module_records.rs +++ b/src/engine/heap/module_records.rs @@ -3,19 +3,19 @@ use super::*; /// Runtime-internal module identity. `cache` is the defining Context whose /// loaded-module cache owns `module`; all dependency indices in that record /// refer to the same cache. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash)] pub(crate) struct RawModuleRef { pub(crate) cache: ContextId, pub(crate) module: ModuleId, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) struct RawPublishedModuleExport { pub(crate) export_name: JsString, pub(crate) target: RawPublishedModuleExportTarget, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) enum RawPublishedModuleExportTarget { SourceTextLocal { closure_index: u16, @@ -29,7 +29,7 @@ pub(crate) enum RawPublishedModuleExportTarget { }, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) enum RawModuleRecordBody { /// Source-text module definition published before parsing begins. /// @@ -50,7 +50,7 @@ pub(crate) enum RawModuleRecordBody { Aborted, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum RawModuleResolutionState { Unresolved, Resolving, @@ -62,7 +62,7 @@ pub(crate) enum RawModuleResolutionState { Resolved(Rc<[ModuleId]>), } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) struct RawModuleInstance { /// Borrowed module snapshots share the slot vector. Copy only when a binding /// is installed, never for each export lookup during namespace construction. @@ -70,7 +70,7 @@ pub(crate) struct RawModuleInstance { pub(crate) callable: Option, } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug)] pub(crate) enum RawModuleNamespaceState { Empty, Building(ObjectId), @@ -85,7 +85,7 @@ pub(crate) enum RawModuleLinkStatus { Poisoned, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) enum RawModuleEvaluationState { Unevaluated, Evaluating, @@ -98,7 +98,7 @@ pub(crate) enum RawModuleEvaluationState { /// First-execution realm retained by a linked module record. The defining /// cache realm is represented without a heap edge because the Context already /// owns the loaded-module record; only a distinct realm is an outgoing edge. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug)] pub(crate) enum RawModuleLinkRealm { Cache, Other(ContextId), @@ -142,7 +142,7 @@ pub(crate) enum RawModuleTransition { /// A snapshot must therefore not outlive the cache root or be used after a /// mutation releases one of its raw fields unless that field was promoted to /// an owning runtime root first. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) struct RawModuleRecord { pub(crate) name: JsString, pub(crate) body: RawModuleRecordBody, @@ -197,7 +197,7 @@ pub(crate) struct RawModuleRecord { pub(crate) compile_realm: ContextId, } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub(crate) struct ModuleLookupIndexes { exports_by_name: HashMap, imports_by_closure: HashMap, @@ -363,7 +363,7 @@ pub(in crate::engine::heap) fn aborted_module_record(record: &RawModuleRecord) - /// compacted or reused: rollback changes an unreferenced slot to `None` and a /// referenced slot to `Aborted`, while the name map continues to point at the /// oldest remaining live record. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub(crate) struct LoadedModuleCache { pub(in crate::engine::heap) records: Vec>, pub(in crate::engine::heap) first_by_name: HashMap, diff --git a/src/engine/heap/module_storage.rs b/src/engine/heap/module_storage.rs index fed581b6..b1c48a3b 100644 --- a/src/engine/heap/module_storage.rs +++ b/src/engine/heap/module_storage.rs @@ -1450,7 +1450,7 @@ impl Heap { } let mut newly_zero = 0usize; for (&edge, &removed) in &counts { - let strong = self.live_node(edge)?.strong; + let strong = self.live_node(edge)?.strong.get(); let remaining = strong.checked_sub(removed).ok_or(HeapError::Underflow { kind: edge.kind(), index: edge.index(), diff --git a/src/engine/heap/object_records.rs b/src/engine/heap/object_records.rs index 28a07df1..c08fbcaf 100644 --- a/src/engine/heap/object_records.rs +++ b/src/engine/heap/object_records.rs @@ -1,7 +1,7 @@ use super::*; /// Parallel property payload for one shape entry. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum PropertySlot { Data(RawValue), /// QuickJS `JS_PROP_VARREF`: an ordinary data descriptor whose mutable @@ -19,7 +19,7 @@ pub enum PropertySlot { /// Typed autoinit payloads. Keeping the creation realm in the per-object slot /// mirrors QuickJS's `JSProperty.u.init.realm_and_id` and allows objects which /// share a shape to retain different realms. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash)] pub enum AutoInitProperty { FunctionPrototype { realm: ContextId, @@ -65,7 +65,7 @@ pub enum AutoInitProperty { /// New variants are added only with their complete class slice so Symbol atom /// ownership and String exotic storage cannot be accidentally skipped by a /// prematurely generic raw-value container. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum PrimitiveObjectData { Number(f64), /// Exact UTF-16 backing store for a genuine String wrapper. Unlike Symbol, @@ -98,7 +98,7 @@ impl PrimitiveObjectData { /// explicit uninitialized state preserves that observable allocation/error /// order. Compiled programs and their source strings are reference-counted /// leaves outside the GC arena and own no heap or atom edge. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum RegExpObjectData { Uninitialized, Compiled { @@ -113,7 +113,7 @@ pub enum RegExpObjectData { /// be referenced by an active native call. `is_callable` is fixed at creation /// time, while the object's constructor bit independently mirrors the target's /// initial `[[Construct]]` capability. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct ProxyData { pub target: ObjectId, pub handler: ObjectId, @@ -127,7 +127,7 @@ pub struct ProxyData { /// resolve/reject pair shares one first-call-wins bit without introducing a /// `Runtime -> heap -> Runtime` ownership cycle. Every raw object identity in /// this enum is still an ordinary traced and reference-counted heap edge. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) enum InternalCallableData { /// `Proxy.revocable`'s one-shot revocation closure. The edge is released /// after the first call, matching QuickJS's `func_data[0] = JS_NULL`. @@ -209,7 +209,7 @@ pub(crate) enum InternalCallableData { // embedder. Public callers may still inspect that a payload is native with // `internal: _` without naming the hidden capture type. #[allow(private_interfaces)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum ObjectPayload { Ordinary, /// Runtime-wide, unforgeable `JS_CLASS_RAWJSON` brand. The exact source @@ -462,7 +462,7 @@ pub enum ObjectKind { /// /// The shape entries and slots are parallel arrays and must have identical /// lengths and storage kinds. Allocation validates that invariant. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct ObjectData { pub shape: ShapeId, diff --git a/src/engine/heap/object_storage.rs b/src/engine/heap/object_storage.rs index f52828cd..b556e2ac 100644 --- a/src/engine/heap/object_storage.rs +++ b/src/engine/heap/object_storage.rs @@ -30,12 +30,23 @@ impl Heap { NodeData::Shape(_) | NodeData::VarRef(_) | NodeData::Context(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed object lookup reached another node payload", )), } } + /// Trusted shared read for a live `ObjectId` held by an owning root. + #[inline] + pub(crate) fn object_fast(&self, id: ObjectId) -> &ObjectData { + match &self.live_node_fast(RawId::Object(id)).data { + NodeData::Object(object) => object, + _ => unreachable!("trusted object handle reached another node payload"), + } + } + /// Set QuickJS's identity-local Annex B `is_HTMLDDA` bit. #[cfg(feature = "test262-host")] pub(crate) fn set_object_is_html_dda(&mut self, id: ObjectId) -> Result<(), HeapError> { @@ -111,12 +122,23 @@ impl Heap { NodeData::Object(_) | NodeData::VarRef(_) | NodeData::Context(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed shape lookup reached another node payload", )), } } + /// Trusted shared read for a live `ShapeId` reachable from a live object. + #[inline] + pub(crate) fn shape_fast(&self, id: ShapeId) -> &Shape { + match &self.live_node_fast(RawId::Shape(id)).data { + NodeData::Shape(shape) => shape, + _ => unreachable!("trusted shape handle reached another node payload"), + } + } + pub(in crate::engine::heap) fn shape_mut( &mut self, id: ShapeId, @@ -129,7 +151,9 @@ impl Heap { NodeData::Object(_) | NodeData::VarRef(_) | NodeData::Context(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed mutable shape lookup reached another node payload", )), } @@ -142,7 +166,9 @@ impl Heap { NodeData::Object(_) | NodeData::Shape(_) | NodeData::VarRef(_) - | NodeData::FunctionBytecode(_) => Err(HeapError::Invariant( + | NodeData::FunctionBytecode(_) + | NodeData::String(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( "typed context lookup reached another node payload", )), } @@ -729,23 +755,23 @@ impl Heap { "in-place property append reached a shared shape", )); } - let index = - self.shape(shape_id)? - .unique_append_index(atom) - .map_err(|error| match error { - ShapeError::NullAtom => { - HeapError::Invariant("in-place property append used a null atom") - } - ShapeError::DuplicateAtom(_) => { - HeapError::Invariant("in-place property append duplicated a shape atom") - } - ShapeError::MissingAtom(_) => HeapError::Invariant( - "in-place property append reported an impossible missing atom", - ), - ShapeError::PropertyIndexOverflow => HeapError::Overflow { - operation: "appending an in-place shape property", - }, - })?; + let index = self + .shape(shape_id)? + .unique_append_index(AtomIdx::from_raw(atom.raw())) + .map_err(|error| match error { + ShapeError::NullAtom => { + HeapError::Invariant("in-place property append used a null atom") + } + ShapeError::DuplicateAtom(_) => { + HeapError::Invariant("in-place property append duplicated a shape atom") + } + ShapeError::MissingAtom(_) => HeapError::Invariant( + "in-place property append reported an impossible missing atom", + ), + ShapeError::PropertyIndexOverflow => HeapError::Overflow { + operation: "appending an in-place shape property", + }, + })?; self.append_unique_object_property_at_index( id, shape_id, @@ -833,7 +859,7 @@ impl Heap { Ok(shape) => shape, Err(_) => unreachable!("authenticated unique shape disappeared before append"), }; - shape.append_unique_property(atom, flags, index); + shape.append_unique_property(AtomIdx::from_raw(atom.raw()), flags, index); let object = match self.object_mut(id) { Ok(object) => object, Err(_) => unreachable!("authenticated object disappeared before slot append"), @@ -1680,7 +1706,8 @@ impl Heap { } if let ObjectPayload::Promise(data) = &object.payload { if !is_promise_storable_value(&data.result) - || (data.state == PromiseState::Pending && data.result != RawValue::Undefined) + || (data.state == PromiseState::Pending + && !matches!(data.result, RawValue::Undefined)) || (data.state != PromiseState::Pending && (!data.fulfill_reactions.is_empty() || !data.reject_reactions.is_empty())) || data @@ -1960,7 +1987,7 @@ impl Heap { )); } } - records.validate()?; + records.validate(self)?; } if let ObjectPayload::MapIterator { object: source, @@ -2005,7 +2032,7 @@ impl Heap { )); } } - records.validate()?; + records.validate(self)?; } if let ObjectPayload::SetIterator { object: source, diff --git a/src/engine/heap/ownership.rs b/src/engine/heap/ownership.rs index d89bc08e..17339570 100644 --- a/src/engine/heap/ownership.rs +++ b/src/engine/heap/ownership.rs @@ -3,7 +3,10 @@ use crate::engine::api::runtime_error::RuntimeError; use crate::engine::atom::{Atom, AtomError}; use crate::engine::heap::runtime::{DeferredRefOp, RuntimeOperation, RuntimeState}; -use crate::engine::heap::{ContextId, FunctionBytecodeId, HeapError, ObjectId, RawId, VarRefId}; +use crate::engine::heap::{ + BigIntId, ContextId, FunctionBytecodeId, HeapError, ObjectId, RawId, RawValue, StringId, + VarRefId, +}; impl Runtime { #[inline] @@ -41,54 +44,102 @@ impl Runtime { } #[inline] + #[track_caller] fn release_or_defer(&self, operation: DeferredRefOp) { let result = if let Ok(mut state) = self.0.state.try_borrow_mut() { state.apply_deferred_operation(operation) } else { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!( + "[defer] {operation:?} at {}", + std::panic::Location::caller() + ); + } self.0.deferred_references.push_back(operation); // The state is still borrowed. The next existing operation boundary // (or a successful release) drains this work after the borrow ends. return; }; - debug_assert!( - result.is_ok(), - "invalid root release {operation:?}: {result:?}" - ); + if std::env::var_os("QJS_TEARDOWN_PROBE").is_some() { + if let Err(error) = &result { + eprintln!("[release] invalid root release {operation:?}: {error:?}"); + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!( + "[release-invalid-backtrace]\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + } + } else { + debug_assert!( + result.is_ok(), + "invalid root release {operation:?}: {result:?}" + ); + } let drain = self.drain_deferred_references(); - debug_assert!(drain.is_ok(), "deferred root release failed: {drain:?}"); + if std::env::var_os("QJS_TEARDOWN_PROBE").is_some() { + if let Err(error) = &drain { + eprintln!("[release] deferred root release failed: {error:?}"); + } + } else { + debug_assert!(drain.is_ok(), "deferred root release failed: {drain:?}"); + } } + #[track_caller] pub(crate) fn retain_object_handle(&self, id: ObjectId) -> Result<(), HeapError> { - let mut state = self.0.state.try_borrow_mut().map_err(|_| { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!("[retain] {id:?} at {}", std::panic::Location::caller()); + } + if let Ok(mut state) = self.0.state.try_borrow_mut() { + return state.heap.retain_object(id); + } + // A nested read may hold a shared state borrow (for example an + // autoinit property materialization rooting its value). The counter + // lives in a `Cell`, so a validated shared-borrow retain is exact. + let state = self.0.state.try_borrow().map_err(|_| { HeapError::Invariant("object root retained during a runtime state borrow") })?; - state.heap.retain_object(id) + state.heap.retain_object_shared(id) } + #[track_caller] pub(crate) fn release_object_handle(&self, id: ObjectId) { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!("[release] {id:?} at {}", std::panic::Location::caller()); + } self.release_or_defer(DeferredRefOp::Object(id)); } pub(crate) fn retain_atom_handle(&self, atom: Atom) -> Result<(), AtomError> { - self.0.state.borrow_mut().atoms.retain(atom).map(drop) + self.0.state.borrow().atoms.retain_shared(atom).map(drop) } pub(crate) fn retain_function_bytecode_handle( &self, id: FunctionBytecodeId, ) -> Result<(), HeapError> { - let mut state = self.0.state.try_borrow_mut().map_err(|_| { + if let Ok(mut state) = self.0.state.try_borrow_mut() { + return state.heap.retain_function_bytecode(id); + } + let state = self.0.state.try_borrow().map_err(|_| { HeapError::Invariant("function bytecode retained during a runtime state borrow") })?; - state.heap.retain_function_bytecode(id) + state.heap.retain_function_bytecode_shared(id) } pub(crate) fn retain_context_handle(&self, id: ContextId) -> Result<(), HeapError> { - let mut state = - self.0.state.try_borrow_mut().map_err(|_| { + if let Ok(mut state) = self.0.state.try_borrow_mut() { + return state.heap.retain_context(id); + } + let state = + self.0.state.try_borrow().map_err(|_| { HeapError::Invariant("context retained during a runtime state borrow") })?; - state.heap.retain_context(id) + state.heap.retain_context_shared(id) } pub(crate) fn release_context_handle(&self, id: ContextId) { @@ -100,11 +151,14 @@ impl Runtime { } pub(crate) fn retain_var_ref_handle(&self, id: VarRefId) -> Result<(), HeapError> { - let mut state = - self.0.state.try_borrow_mut().map_err(|_| { + if let Ok(mut state) = self.0.state.try_borrow_mut() { + return state.heap.retain_var_ref(id); + } + let state = + self.0.state.try_borrow().map_err(|_| { HeapError::Invariant("VarRef retained during a runtime state borrow") })?; - state.heap.retain_var_ref(id) + state.heap.retain_var_ref_shared(id) } pub(crate) fn release_var_ref_handle(&self, id: VarRefId) { @@ -112,7 +166,149 @@ impl Runtime { } pub(crate) fn release_atom_handle(&self, atom: Atom) { - self.release_or_defer(DeferredRefOp::Atom(atom)); + // Shared-borrow release: the counter decrement runs immediately; when + // the last reference drops, slot removal is deferred to the next + // operation boundary through the existing deferred queue. An invalid + // release is re-deferred so the error surfaces at the drain boundary + // exactly like the historical full-release path. + let hit_zero = match self.0.state.try_borrow() { + Ok(state) => match state.atoms.release_shared(atom) { + Ok(hit_zero) => hit_zero, + Err(_) => { + drop(state); + self.0 + .deferred_references + .push_back(DeferredRefOp::AtomRelease(atom)); + return; + } + }, + Err(_) => { + // The table is mutably borrowed (interning/removal in + // progress). Defer the whole shared-release pass; it runs at + // the next safe point. + self.0 + .deferred_references + .push_back(DeferredRefOp::AtomRelease(atom)); + return; + } + }; + if hit_zero { + self.release_or_defer(DeferredRefOp::AtomRemove(atom)); + } + } + + #[track_caller] + pub(crate) fn retain_string_handle(&self, id: StringId) -> Result<(), HeapError> { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!("[retain-s] {id:?} at {}", std::panic::Location::caller()); + if std::env::var("QJS_TRACE_STRING_ID") + .ok() + .and_then(|value| value.parse::().ok()) + == Some(id.index) + { + eprintln!( + "[retain-s-backtrace]\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + } + if let Ok(mut state) = self.0.state.try_borrow_mut() { + return state.heap.retain_string(id); + } + let state = self.0.state.try_borrow().map_err(|_| { + HeapError::Invariant("string node retained during a runtime state borrow") + })?; + state.heap.retain_string_shared(id) + } + + #[track_caller] + pub(crate) fn release_string_handle(&self, id: StringId) { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!("[release-s] {id:?} at {}", std::panic::Location::caller()); + if std::env::var("QJS_TRACE_STRING_ID") + .ok() + .and_then(|value| value.parse::().ok()) + == Some(id.index) + { + eprintln!( + "[release-s-backtrace]\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + } + self.release_or_defer(DeferredRefOp::String(id)); + } + + #[track_caller] + pub(crate) fn retain_bigint_handle(&self, id: BigIntId) -> Result<(), HeapError> { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!("[retain-b] {id:?} at {}", std::panic::Location::caller()); + if std::env::var("QJS_TRACE_BIGINT_ID") + .ok() + .and_then(|value| value.parse::().ok()) + == Some(id.index) + { + eprintln!( + "[retain-b-backtrace]\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + } + if let Ok(mut state) = self.0.state.try_borrow_mut() { + return state.heap.retain_bigint(id); + } + let state = self.0.state.try_borrow().map_err(|_| { + HeapError::Invariant("bigint node retained during a runtime state borrow") + })?; + state.heap.retain_bigint_shared(id) + } + + #[track_caller] + pub(crate) fn release_bigint_handle(&self, id: BigIntId) { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!("[release-b] {id:?} at {}", std::panic::Location::caller()); + if std::env::var("QJS_TRACE_BIGINT_ID") + .ok() + .and_then(|value| value.parse::().ok()) + == Some(id.index) + { + eprintln!( + "[release-b-backtrace]\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + } + self.release_or_defer(DeferredRefOp::BigInt(id)); + } + + /// Release one producer-owned string/BigInt conversion edge after the + /// transactional store has retained its own copy edge. + #[track_caller] + pub(crate) fn release_converted_node_edge(&self, edge: RawId) { + #[cfg(debug_assertions)] + if std::env::var_os("QJS_TRACE_ROOTS").is_some() { + eprintln!( + "[release-converted] {edge:?} at {}", + std::panic::Location::caller() + ); + } + match edge { + RawId::String(id) => self.release_or_defer(DeferredRefOp::String(id)), + RawId::BigInt(id) => self.release_or_defer(DeferredRefOp::BigInt(id)), + _ => unreachable!("conversion edges are only string or bigint node edges"), + } + } + + /// Release the conversion edge carried by a boundary-converted value, if + /// any. See [`RawValue::conversion_node_edge`]. + pub(crate) fn release_converted_value_edge(&self, value: &RawValue) { + if let Some(edge) = value.conversion_node_edge() { + self.release_converted_node_edge(edge); + } } } impl RuntimeState { @@ -137,7 +333,15 @@ impl RuntimeState { self.release_heap_reference(RawId::FunctionBytecode(bytecode)) } DeferredRefOp::VarRef(var_ref) => self.release_heap_reference(RawId::VarRef(var_ref)), - DeferredRefOp::Atom(atom) => self.atoms.release(atom).map(drop).map_err(Into::into), + DeferredRefOp::String(id) => self.release_heap_reference(RawId::String(id)), + DeferredRefOp::BigInt(id) => self.release_heap_reference(RawId::BigInt(id)), + DeferredRefOp::AtomRelease(atom) => { + if self.atoms.release_shared(atom)? { + self.atoms.remove_released(atom)?; + } + Ok(()) + } + DeferredRefOp::AtomRemove(atom) => self.atoms.remove_released(atom).map_err(Into::into), DeferredRefOp::ActiveFramePop { token, depth } => { self.active_frames.retire(token, depth); Ok(()) diff --git a/src/engine/heap/profiling.rs b/src/engine/heap/profiling.rs index 66615e08..b0de83b8 100644 --- a/src/engine/heap/profiling.rs +++ b/src/engine/heap/profiling.rs @@ -116,6 +116,10 @@ impl Heap { let mut buffers = storage("array_buffer_bytes", 0, 0, 1); let mut shared_buffers = logical("shared_array_buffer_wrappers", 0); shared_buffers.basis = "wrapper-count-only; shared backing bytes unavailable"; + let mut string_nodes = logical("string_nodes", 0); + string_nodes.basis = "node-count-only; payloads are Rc-owned outside the arena accounting"; + let mut bigint_nodes = logical("bigint_nodes", 0); + bigint_nodes.basis = "node-count-only; payloads are Rc-owned outside the arena accounting"; let mut code = storage("bytecode_instructions", 0, 0, 1); code.basis = "deduplicated-Rc-slice-inline-bytes; excludes Rc headers and nested operands"; let mut seen_code = HashSet::new(); @@ -191,6 +195,12 @@ impl Heap { keys.iter().filter(|atom| atom.is_null()).count(); } } + NodeData::String(_) => { + *string_nodes.count.as_mut().unwrap() += 1; + } + NodeData::BigInt(_) => { + *bigint_nodes.count.as_mut().unwrap() += 1; + } _ => {} } } @@ -200,6 +210,8 @@ impl Heap { elements, buffers, shared_buffers, + string_nodes, + bigint_nodes, code, property_keys, executable_projections, diff --git a/src/engine/heap/promise_records.rs b/src/engine/heap/promise_records.rs index df4f84ef..f77cd4d9 100644 --- a/src/engine/heap/promise_records.rs +++ b/src/engine/heap/promise_records.rs @@ -1,7 +1,7 @@ use super::*; /// ECMAScript-visible state of one genuine Promise object. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq)] pub enum PromiseState { Pending, Fulfilled, @@ -9,7 +9,7 @@ pub enum PromiseState { } /// Which settlement path owns a Promise reaction record. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum PromiseReactionKind { Fulfill, Reject, @@ -21,14 +21,14 @@ pub enum PromiseReactionKind { /// reaction keeps both callables alive until it is detached or its owning /// Promise is finalized. The result Promise itself is returned synchronously /// from `then`; QuickJS does not retain it as a separate reaction edge. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct PromiseCapabilityData { pub resolve: ObjectId, pub reject: ObjectId, } /// One `PerformPromiseThen` reaction retained by a pending Promise. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct PromiseReaction { pub kind: PromiseReactionKind, pub handler: Option, @@ -42,7 +42,7 @@ pub struct PromiseReaction { /// `result` is `undefined` while pending. Reaction vectors are kept separate /// to preserve QuickJS's fulfill/reject list order, while each record also /// carries its kind so queued jobs remain self-describing after detachment. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct PromiseData { pub state: PromiseState, pub result: RawValue, @@ -52,7 +52,7 @@ pub struct PromiseData { } /// Mutable edge capture owned by an internal NewPromiseCapability executor. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct PromiseCapabilityExecutorData { pub resolve: Option, pub reject: Option, diff --git a/src/engine/heap/realm_records.rs b/src/engine/heap/realm_records.rs index 16eadbcd..90cfe52a 100644 --- a/src/engine/heap/realm_records.rs +++ b/src/engine/heap/realm_records.rs @@ -5,7 +5,7 @@ use super::*; /// prototype, and initial instance shape independently from their public /// property graph because user code may replace or delete those properties /// after bootstrap. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct RegExpRealmData { pub prototype: ObjectId, pub constructor: ObjectId, @@ -21,7 +21,7 @@ pub struct RegExpRealmData { /// iterators. QuickJS roots the two class prototypes, but not the public Map /// constructor: deleting the global and `Map.prototype.constructor` edges may /// therefore make that constructor collectible. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct MapRealmData { pub prototype: ObjectId, /// Realm-local `%MapIteratorPrototype%`, inheriting from this realm's @@ -32,7 +32,7 @@ pub struct MapRealmData { /// Realm-owned identities required to allocate genuine Set objects and their /// iterators. As with Map, QuickJS roots the two class prototypes without /// independently rooting the public Set constructor. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct SetRealmData { pub prototype: ObjectId, /// Realm-local `%SetIteratorPrototype%`, inheriting from this realm's @@ -45,13 +45,13 @@ pub struct SetRealmData { /// Weak collections have no iterator prototype. As in pinned QuickJS, the /// public constructor remains reachable through the ordinary property graph /// rather than through an additional Context edge. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct WeakMapRealmData { pub prototype: ObjectId, } /// Realm-owned `%WeakSet.prototype%` class root. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct WeakSetRealmData { pub prototype: ObjectId, } @@ -60,7 +60,7 @@ pub struct WeakSetRealmData { /// `JS_AddIntrinsicWeakRef` bootstrap step. The public constructors remain /// reachable through their ordinary prototype/global property graph and are /// therefore not duplicated as Context roots. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct WeakRefRealmData { pub weak_ref_prototype: ObjectId, pub finalization_registry_prototype: ObjectId, @@ -73,7 +73,7 @@ pub struct WeakRefRealmData { /// objects inherit from `function_prototype`, while generator instances use /// `prototype` as the cross-realm fallback when a callable's public /// `.prototype` is not an object. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct GeneratorRealmData { pub prototype: ObjectId, pub function_prototype: ObjectId, @@ -85,7 +85,7 @@ pub struct GeneratorRealmData { /// a direct child of the realm's `%Function.prototype%`. The hidden /// `AsyncFunction` constructor remains reachable through the reciprocal /// property graph and therefore needs no independent Context root. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct AsyncFunctionRealmData { pub function_prototype: ObjectId, } @@ -95,7 +95,7 @@ pub struct AsyncFunctionRealmData { /// QuickJS keeps `%AsyncIteratorPrototype%`, `%AsyncGeneratorPrototype%`, and /// `%AsyncGeneratorFunction.prototype%` as independent context roots. The /// hidden dynamic constructor remains reachable from the reciprocal graph. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct AsyncGeneratorRealmData { pub async_iterator_prototype: ObjectId, /// Realm-local `%AsyncFromSyncIteratorPrototype%`, inheriting from this @@ -110,7 +110,7 @@ pub struct AsyncGeneratorRealmData { /// Both identities remain explicit Context roots. User code may delete the /// public global and constructor/prototype properties without changing the /// intrinsic identities used by Promise abstract operations. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct PromiseRealmData { pub prototype: ObjectId, pub constructor: ObjectId, @@ -124,7 +124,7 @@ pub struct PromiseRealmData { /// one transaction, matching QuickJS's `iterator_ctor` and class-prototype /// roots for `JS_CLASS_ITERATOR_CONCAT`, `JS_CLASS_ITERATOR_HELPER`, and /// `JS_CLASS_ITERATOR_WRAP`. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct IteratorRealmData { pub constructor: ObjectId, pub concat_prototype: ObjectId, @@ -137,7 +137,7 @@ pub struct IteratorRealmData { /// The backing bytes live on each branded object, but constructor-realm /// fallback must retain the original prototype even after authored code /// replaces or deletes the writable global `ArrayBuffer` binding. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct ArrayBufferRealmData { pub prototype: ObjectId, } @@ -146,7 +146,7 @@ pub struct ArrayBufferRealmData { /// /// Shared wrappers may outlive and share backing stores across runtimes, but /// their JavaScript prototype identity remains owned by the importing realm. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct SharedArrayBufferRealmData { pub prototype: ObjectId, } @@ -155,7 +155,7 @@ pub struct SharedArrayBufferRealmData { /// /// DataView instances retain their backing ArrayBuffer-family object directly. The realm /// keeps only the original prototype identity used by constructor fallback. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct DataViewRealmData { pub prototype: ObjectId, } @@ -165,7 +165,7 @@ pub struct DataViewRealmData { /// QuickJS roots these twelve identities in `ctx->class_proto`. The hidden /// abstract prototype stays reachable through their `[[Prototype]]` edges; /// retaining it separately here would add an arena root that upstream lacks. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug)] pub struct TypedArrayRealmData { pub prototypes: [ObjectId; TypedArrayElementKind::COUNT], } @@ -175,7 +175,7 @@ pub struct TypedArrayRealmData { /// The bootstrap roots needed by ordinary script evaluation are explicit; /// additional intrinsic and module roots can extend the vectors without /// changing `ContextId` ownership. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct ContextData { /// Stable public handle identity assigned by `Runtime::new_context`. /// diff --git a/src/engine/heap/realm_storage.rs b/src/engine/heap/realm_storage.rs index bd1ce02c..756af45d 100644 --- a/src/engine/heap/realm_storage.rs +++ b/src/engine/heap/realm_storage.rs @@ -413,7 +413,7 @@ impl Heap { "RegExp object shape does not contain exactly one lastIndex property", )); }; - if last_index.atom != last_index_atom + if last_index.atom != AtomIdx::from_raw(last_index_atom.raw()) || last_index.flags != PropertyFlags::data(true, false, false) { return Err(HeapError::Invariant( diff --git a/src/engine/heap/roots.rs b/src/engine/heap/roots.rs index 3102b804..106ed67e 100644 --- a/src/engine/heap/roots.rs +++ b/src/engine/heap/roots.rs @@ -1,27 +1,30 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; use crate::engine::code::function::metadata::{ClosureVariable, ClosureVariableKind}; use crate::engine::heap::{HeapError, RawValue, VarRefData, VarRefId}; use crate::engine::object::{ObjectRef, SymbolRef}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::bindings::closure_view_matches_cell; impl Runtime { + /// Store an internal value into a fresh captured cell, consuming the + /// value's edges. The cell retains its own copy edge transactionally; + /// the passed value's edges are released before returning. pub(crate) fn new_var_ref( &self, - value: Value, + value: JsValue, is_lexical: bool, is_const: bool, kind: ClosureVariableKind, ) -> Result { let _operation = self.operation(); - self.validate_value_domain(&value, "captured variable")?; - let raw = self.raw_property_value(&value)?; + let raw = value.as_raw(); let mut state = self.0.state.borrow_mut(); - let retained_atom = if let RawValue::Symbol(atom) = &raw { - state.atoms.retain(*atom)?; - Some(*atom) + let retained_atom = if let RawValue::Symbol(index) = &raw { + state.atoms.retain_index(*index)?; + Some(*index) } else { None }; @@ -29,17 +32,36 @@ impl Runtime { let id = match state.heap.allocate_var_ref(data) { Ok(id) => id, Err(error) => { - if let Some(atom) = retained_atom { - state.atoms.release(atom)?; + if let Some(index) = retained_atom { + state.atoms.release_index(index)?; } + drop(state); + self.release_jsvalue(value)?; return Err(error.into()); } }; drop(state); - drop(value); + // The cell retained its own copy edge; the consumed value's edge + // is no longer needed. + self.release_jsvalue(value)?; Ok(VarRefRoot::from_owned_handle(self.clone(), id)) } + /// Public-root boundary form of [`Runtime::new_var_ref`]: converts the + /// root into an internal value (allocating string/BigInt nodes) and + /// consumes it. + pub(crate) fn new_var_ref_rooted( + &self, + value: Value, + is_lexical: bool, + is_const: bool, + kind: ClosureVariableKind, + ) -> Result { + self.validate_value_domain(&value, "captured variable")?; + let value = self.unroot_value(&value)?; + self.new_var_ref(value, is_lexical, is_const, kind) + } + pub(crate) fn new_uninitialized_var_ref(&self) -> Result { let _operation = self.operation(); let id = self @@ -110,10 +132,12 @@ impl Runtime { state.apply_cleanup(cleanup) } + /// Read a captured cell as an owned internal value, duplicating every + /// heap edge the cell carries. pub(crate) fn read_var_ref( &self, root: &impl crate::engine::heap::roots::VarRefHandle, - ) -> Result { + ) -> Result { let _operation = self.operation(); if !root.belongs_to(self) { return Err(RuntimeError::WrongRuntime("closure variable")); @@ -128,7 +152,23 @@ impl Runtime { } var_ref.value.clone() }; - self.root_raw_value(&raw) + let mut state = self.0.state.borrow_mut(); + state.retain_raw_root(&raw)?; + JsValue::from_raw(raw).ok_or(RuntimeError::Invariant( + "internal value sentinel occupied a captured variable cell", + )) + } + + /// Public-root boundary form of [`Runtime::read_var_ref`]. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn read_var_ref_rooted( + &self, + root: &impl crate::engine::heap::roots::VarRefHandle, + ) -> Result { + let value = self.read_var_ref(root)?; + let rooted = self.root_value(&value); + self.release_jsvalue(value)?; + rooted } /// Root a fresh non-immediate cell value without entering an operation or @@ -137,7 +177,7 @@ impl Runtime { pub(crate) fn try_read_owned_var_ref( &self, root: &impl crate::engine::heap::roots::VarRefHandle, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { if !root.belongs_to(self) || self.0.deferred_references.has_pending() { return Ok(None); } @@ -162,14 +202,64 @@ impl Runtime { return Ok(None); } let raw = cell.value.clone(); - // Object and Symbol retain check overflow before incrementing. String - // and BigInt ownership is already carried by the cloned raw value. - // Thus an error leaves the cell and reference counts unchanged. + // The cell keeps its own edge; this read retains one new edge for + // every heap-backed kind before handing out the owned value. state.retain_raw_root(&raw)?; drop(state); - // The selected public variants cannot fail conversion. This shared - // constructor consumes the retained owner without another retain. - self.take_owned_raw_value(raw).map(Some) + Ok(Some(JsValue::from_raw(raw).ok_or( + RuntimeError::Invariant("internal value sentinel occupied a captured cell"), + )?)) + } + + /// Trusted shared-borrow read of a proven live captured cell. + /// + /// Handles the object, string, BigInt and symbol cases without a mutable + /// state borrow: node payloads clone their inner `Rc`, objects take the + /// trusted retain fast path, and symbols retain through the atom table's + /// shared-borrow `Cell` counter. A declined read claims no owner and + /// leaves the cell unchanged. + #[inline] + pub(crate) fn read_owned_cell_fast( + &self, + root: &impl crate::engine::heap::roots::VarRefHandle, + ) -> Option { + if !root.belongs_to(self) || self.0.deferred_references.has_pending() { + return None; + } + let state = self.0.state.try_borrow().ok()?; + if !state.heap.zero_queue.is_empty() { + return None; + } + let cell = state.heap.var_ref_fast(root.id()); + if cell.kind.is_private() { + return None; + } + match &cell.value { + RawValue::Object(object) => { + state.heap.retain_object_fast(*object); + Some(JsValue::Object(*object)) + } + RawValue::String(id) => { + state.heap.retain_string_shared(*id).ok()?; + Some(JsValue::String(*id)) + } + RawValue::BigInt(id) => { + state.heap.retain_bigint_shared(*id).ok()?; + Some(JsValue::BigInt(*id)) + } + RawValue::Symbol(index) => { + state.atoms.retain_index_shared(*index).ok()?; + Some(JsValue::Symbol(*index)) + } + RawValue::Undefined + | RawValue::Null + | RawValue::Bool(_) + | RawValue::Int(_) + | RawValue::Float(_) + | RawValue::Private(_) + | RawValue::Uninitialized + | RawValue::Exception => None, + } } /// Guarded global own-data read for an unresolved, non-lexical binding. @@ -181,7 +271,7 @@ impl Runtime { root: &impl VarRefHandle, realm: crate::engine::heap::ContextId, atom: crate::engine::atom::Atom, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { use crate::engine::heap::{ObjectKind, ObjectPayload, PropertySlot}; use crate::engine::object::shape::PropertyStorageKind; if !root.belongs_to(self) || self.0.deferred_references.has_pending() { @@ -220,7 +310,7 @@ impl Runtime { let index = if let Some(entry) = cached { entry.index } else { - let Some(index) = shape.find(atom) else { + let Some(index) = shape.find(AtomIdx::from_raw(atom.raw())) else { cell.global_location.set(None); return Ok(None); }; @@ -260,7 +350,9 @@ impl Runtime { let raw = raw.clone(); state.retain_raw_root(&raw)?; drop(state); - self.take_owned_raw_value(raw).map(Some) + Ok(Some(JsValue::from_raw(raw).ok_or( + RuntimeError::Invariant("internal value sentinel occupied a global binding cell"), + )?)) } pub(crate) fn raw_var_ref_value( @@ -296,10 +388,14 @@ impl Runtime { Ok(()) } + /// Replace a captured cell's value, consuming the passed value's edges. + /// The cell retains its own copy edge transactionally and the previous + /// value's edges are released by the replacement; the passed value's + /// edges are released before returning. pub(crate) fn write_var_ref( &self, root: &impl crate::engine::heap::roots::VarRefHandle, - value: Value, + value: JsValue, ) -> Result<(), RuntimeError> { let _operation = self.operation(); if !root.belongs_to(self) { @@ -318,40 +414,64 @@ impl Runtime { "ordinary VarRef write reached a private-element binding", )); } - self.validate_value_domain(&value, "captured variable")?; - let raw = self.raw_property_value(&value)?; + let raw = value.as_raw(); let mut state = self.0.state.borrow_mut(); - let retained_atom = if let RawValue::Symbol(atom) = &raw { - state.atoms.retain(*atom)?; - Some(*atom) + let retained_atom = if let RawValue::Symbol(index) = &raw { + state.atoms.retain_index(*index)?; + Some(*index) } else { None }; let cleanup = match state.heap.replace_var_ref_value(root.id(), raw) { Ok(cleanup) => cleanup, Err(error) => { - if let Some(atom) = retained_atom { - state.atoms.release(atom)?; + if let Some(index) = retained_atom { + state.atoms.release_index(index)?; } + drop(state); + self.release_jsvalue(value)?; return Err(error.into()); } }; state.apply_cleanup(cleanup)?; drop(state); - drop(value); + // The cell retained its own copy edge; the consumed value's edge + // is no longer needed. + self.release_jsvalue(value)?; Ok(()) } + /// Public-root boundary form of [`Runtime::write_var_ref`]. + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn write_var_ref_rooted( + &self, + root: &impl crate::engine::heap::roots::VarRefHandle, + value: Value, + ) -> Result<(), RuntimeError> { + self.validate_value_domain(&value, "captured variable")?; + let value = self.unroot_value(&value)?; + self.write_var_ref(root, value) + } + pub(crate) fn take_owned_raw_value(&self, value: RawValue) -> Result { - Ok(match value { + // The caller consumed one owned root edge for this value. Object and + // Symbol edges transfer into the public root wrappers below; a string + // or BigInt node edge does not (the public value owns an `Rc` payload + // clone), so it is released once the payload has been read out. + let node_edge = value.conversion_node_edge(); + let state = self.0.state.borrow(); + let converted = Ok(match value { RawValue::Undefined => Value::Undefined, RawValue::Null => Value::Null, RawValue::Bool(value) => Value::Bool(value), RawValue::Int(value) => Value::Int(value), RawValue::Float(value) => Value::Float(value), - RawValue::BigInt(value) => Value::BigInt(value), - RawValue::String(value) => Value::String(value), - RawValue::Symbol(atom) => Value::Symbol(SymbolRef::from_owned_atom(self.clone(), atom)), + RawValue::BigInt(id) => Value::BigInt(state.heap.bigint(id)?.clone()), + RawValue::String(id) => Value::String(state.heap.string(id)?.clone()), + RawValue::Symbol(index) => { + let atom = state.atoms.brand(index)?; + Value::Symbol(SymbolRef::from_owned_atom(self.clone(), atom)) + } RawValue::Private(_) => { return Err(RuntimeError::Invariant( "private-name identity occupied a public runtime root", @@ -365,20 +485,27 @@ impl Runtime { "internal value sentinel occupied the pending exception slot", )); } - }) + }); + drop(state); + if let Some(edge) = node_edge { + self.release_converted_node_edge(edge); + } + converted } pub(crate) fn root_raw_value(&self, value: &RawValue) -> Result { + let state = self.0.state.borrow(); Ok(match value { RawValue::Undefined => Value::Undefined, RawValue::Null => Value::Null, RawValue::Bool(value) => Value::Bool(*value), RawValue::Int(value) => Value::Int(*value), RawValue::Float(value) => Value::Float(*value), - RawValue::BigInt(value) => Value::BigInt(value.clone()), - RawValue::String(value) => Value::String(value.clone()), - RawValue::Symbol(atom) => { - Value::Symbol(SymbolRef::from_borrowed_atom(self.clone(), *atom)?) + RawValue::BigInt(id) => Value::BigInt(state.heap.bigint(*id)?.clone()), + RawValue::String(id) => Value::String(state.heap.string(*id)?.clone()), + RawValue::Symbol(index) => { + let atom = state.atoms.brand(*index)?; + Value::Symbol(SymbolRef::from_borrowed_atom(self.clone(), atom)?) } RawValue::Private(_) => { return Err(RuntimeError::Invariant( @@ -463,13 +590,14 @@ mod owned_cell_tests { .try_read_unresolved_global(&root, context.realm, atom) .unwrap() .unwrap(); - assert!(matches!(first, Value::Object(_))); + assert!(matches!(&first, JsValue::Object(_))); + runtime.release_jsvalue(first).unwrap(); context.eval("nativeLeaf = 7").unwrap(); assert_eq!( runtime .try_read_unresolved_global(&root, context.realm, atom) .unwrap(), - Some(Value::Int(7)) + Some(JsValue::Int(7)) ); context.eval("Object.defineProperty(globalThis, 'nativeLeaf', { get() { throw 99; }, configurable: true })").unwrap(); assert!( @@ -511,7 +639,7 @@ mod owned_cell_tests { for source in ["({})", "Symbol('cell')", "'cell'", "123456789012345678901n"] { let value = context.eval(source).unwrap(); let root = runtime - .new_var_ref(value.clone(), false, false, ClosureVariableKind::Normal) + .new_var_ref_rooted(value.clone(), false, false, ClosureVariableKind::Normal) .unwrap(); let object_id = match &value { Value::Object(object) => Some(object.object_id()), @@ -527,7 +655,7 @@ mod owned_cell_tests { .unwrap() }); let copied = runtime.try_read_owned_var_ref(&root).unwrap().unwrap(); - assert_eq!(copied, value); + assert_eq!(runtime.root_value(&copied).unwrap(), value); if let (Some(id), Some(before)) = (object_id, before) { assert_eq!( runtime @@ -541,12 +669,12 @@ mod owned_cell_tests { ); } drop(value); - runtime.write_var_ref(&root, Value::Int(1)).unwrap(); + runtime.write_var_ref(&root, JsValue::Int(1)).unwrap(); runtime.run_gc().unwrap(); if let Some(id) = object_id { assert!(runtime.0.state.borrow().heap.object(id).is_ok()); } - drop(copied); + runtime.release_jsvalue(copied).unwrap(); runtime.run_gc().unwrap(); if let Some(id) = object_id { assert!(runtime.0.state.borrow().heap.object(id).is_err()); @@ -559,7 +687,7 @@ mod owned_cell_tests { let runtime = Runtime::new(); let foreign = Runtime::new(); let root = runtime - .new_var_ref( + .new_var_ref_rooted( Value::Object(runtime.new_object(None).unwrap()), false, false, @@ -604,7 +732,11 @@ mod owned_cell_tests { let cleanup = state.heap.drain_zero_queue().unwrap(); state.apply_cleanup(cleanup).unwrap(); } - assert!(runtime.try_read_owned_var_ref(&root).unwrap().is_some()); + let read = runtime.try_read_owned_var_ref(&root).unwrap(); + assert!(read.is_some()); + if let Some(value) = read { + runtime.release_jsvalue(value).unwrap(); + } runtime.reset_var_ref_uninitialized(&root).unwrap(); assert!(runtime.try_read_owned_var_ref(&root).unwrap().is_none()); } @@ -615,7 +747,7 @@ mod owned_cell_tests { let value = runtime.new_object(None).unwrap(); let id = value.object_id(); let root = runtime - .new_var_ref( + .new_var_ref_rooted( Value::Object(value), false, false, @@ -636,7 +768,8 @@ mod owned_cell_tests { .heap .live_node_mut(RawId::Object(id)) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); let result = runtime.try_read_owned_var_ref(&root); let after = runtime .0 @@ -653,13 +786,14 @@ mod owned_cell_tests { .heap .live_node_mut(RawId::Object(id)) .unwrap() - .strong = before; + .strong + .set(before); assert!(result.is_err()); assert_eq!(after, u32::MAX); - assert_eq!( + assert!(matches!( runtime.raw_var_ref_value(&root).unwrap(), - RawValue::Object(id) - ); + RawValue::Object(object) if object == id + )); } } diff --git a/src/engine/heap/runtime/builtin_batch_tests.rs b/src/engine/heap/runtime/builtin_batch_tests.rs index 335454c4..65d4eb35 100644 --- a/src/engine/heap/runtime/builtin_batch_tests.rs +++ b/src/engine/heap/runtime/builtin_batch_tests.rs @@ -21,6 +21,12 @@ fn layout(runtime: &Runtime, object: &ObjectRef) -> (ShapeId, Vec) (object.shape, object.slots.clone()) } +/// `PropertySlot` has no `PartialEq` (its payloads embed `RawValue`), so +/// layout snapshots compare through their debug rendering instead. +fn layout_summary(layout: &(ShapeId, Vec)) -> (ShapeId, String) { + (layout.0, format!("{:?}", layout.1)) +} + #[test] fn builtin_batch_preserves_order_flags_metadata_and_lazy_identity() { let runtime = Runtime::new(); @@ -41,21 +47,29 @@ fn builtin_batch_preserves_order_flags_metadata_and_lazy_identity() { assert_eq!( state .atoms - .to_js_string(entries[index].atom) + .to_js_string(state.atoms.brand(entries[index].atom).unwrap()) .unwrap() .to_string(), method.name ); assert_eq!(entries[index].flags, method.flags); - assert_eq!( - object.slots[index], - PropertySlot::AutoInit(AutoInitProperty::NativeBuiltin { - realm: context.realm, - target: method.target, - name: method.name, - length: method.length, - min_readable_args: method.min_readable_args, - }) + assert!( + matches!( + &object.slots[index], + PropertySlot::AutoInit(AutoInitProperty::NativeBuiltin { + realm, + target, + name, + length, + min_readable_args, + }) if *realm == context.realm + && *target == method.target + && *name == method.name + && *length == method.length + && *min_readable_args == method.min_readable_args + ), + "slot mismatch: {:?}", + object.slots[index] ); } } @@ -104,20 +118,29 @@ fn builtin_batch_rejects_entire_invalid_table_without_leaking_keys() { .define_native_builtin_auto_init_batch(&object, context.realm, methods) .is_err() ); - assert_eq!(layout(&runtime, &object), original); + assert_eq!( + layout_summary(&layout(&runtime, &object)), + layout_summary(&original) + ); assert_eq!(runtime.test_atom_count(), atoms); } runtime .define_native_builtin_auto_init_batch(&object, context.realm, []) .unwrap(); - assert_eq!(layout(&runtime, &object), original); + assert_eq!( + layout_summary(&layout(&runtime, &object)), + layout_summary(&original) + ); runtime.prevent_extensions(&object).unwrap(); assert!( runtime .define_native_builtin_auto_init_batch(&object, context.realm, [method("batch_new")]) .is_err() ); - assert_eq!(layout(&runtime, &object), original); + assert_eq!( + layout_summary(&layout(&runtime, &object)), + layout_summary(&original) + ); } #[test] @@ -147,7 +170,10 @@ fn builtin_batch_validates_receiver_domain_and_realm_lifetime() { .define_native_builtin_auto_init_batch(&object, realm, [method("batch_stale")]) .is_err() ); - assert_eq!(layout(&runtime, &object), original); + assert_eq!( + layout_summary(&layout(&runtime, &object)), + layout_summary(&original) + ); assert_eq!(runtime.test_atom_count(), atoms); } @@ -165,8 +191,8 @@ fn builtin_batch_rolls_back_shape_and_realm_edges_on_retain_overflow() { .heap .live_node_mut(RawId::Context(context.realm)) .unwrap(); - let strong = node.strong; - node.strong = u32::MAX; + let strong = node.strong.get(); + node.strong.set(u32::MAX); (strong, counts.live, counts.shape_nodes) }; let result = runtime.define_native_builtin_auto_init_batch( @@ -180,14 +206,17 @@ fn builtin_batch_rolls_back_shape_and_realm_edges_on_retain_overflow() { .heap .live_node_mut(RawId::Context(context.realm)) .unwrap(); - let after = node.strong; - node.strong = strong; + let after = node.strong.get(); + node.strong.set(strong); assert_eq!(after, u32::MAX); assert_eq!(state.heap.counts().live, live); assert_eq!(state.heap.counts().shape_nodes, shapes); } assert!(result.is_err()); - assert_eq!(layout(&runtime, &object), original); + assert_eq!( + layout_summary(&layout(&runtime, &object)), + layout_summary(&original) + ); assert_eq!(runtime.test_atom_count(), atoms); } @@ -283,7 +312,10 @@ fn builtin_batch_rejects_exotic_receivers_without_observable_traps() { ) .is_err() ); - assert_eq!(layout(&runtime, &object), original); + assert_eq!( + layout_summary(&layout(&runtime, &object)), + layout_summary(&original) + ); assert_eq!(runtime.test_atom_count(), atoms); } } diff --git a/src/engine/heap/runtime/layout.rs b/src/engine/heap/runtime/layout.rs index d80dbc47..a8ba0dc4 100644 --- a/src/engine/heap/runtime/layout.rs +++ b/src/engine/heap/runtime/layout.rs @@ -47,13 +47,15 @@ mod tests { let before_key = state.atoms.resolve(key.atom()).unwrap().ref_count; let before_symbol = state.atoms.resolve(symbol.atom()).unwrap().ref_count; let entries = [ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: PropertyFlags::accessor(true, true), }]; let result = state.allocate_object_with_layout( None, &entries, - vec![PropertySlot::Data(RawValue::Symbol(symbol.atom()))], + vec![PropertySlot::Data(RawValue::Symbol(AtomIdx::from_raw( + symbol.atom().raw(), + )))], ObjectData::ordinary, ); assert!( diff --git a/src/engine/heap/runtime/mod.rs b/src/engine/heap/runtime/mod.rs index 0ebc0dce..664b010e 100644 --- a/src/engine/heap/runtime/mod.rs +++ b/src/engine/heap/runtime/mod.rs @@ -13,11 +13,11 @@ use crate::engine::host::HostServices; use crate::engine::{builtins as intrinsics, jobs, modules as module}; -use crate::engine::atom::{Atom, AtomTable}; +use crate::engine::atom::{Atom, AtomIdx, AtomTable}; use crate::engine::code::debug::DebugInfoMode; use crate::engine::heap::{ - ContextId, FunctionBytecodeId, Heap, HeapCleanup, ObjectId, PropertySlot, RawValue, ShapeId, - VarRefId, + BigIntId, ContextId, FunctionBytecodeId, Heap, HeapCleanup, ObjectId, PropertySlot, RawValue, + ShapeId, StringId, VarRefId, }; use crate::engine::object::WellKnownSymbol; use crate::engine::object::shape::{Shape, ShapeEntry}; @@ -67,7 +67,14 @@ pub(crate) enum DeferredRefOp { Context(ContextId), FunctionBytecode(FunctionBytecodeId), VarRef(VarRefId), - Atom(Atom), + String(StringId), + BigInt(BigIntId), + /// Shared-release pass deferred because the table was mutably borrowed: + /// decrement the counter, then remove the slot if it reached zero. + AtomRelease(Atom), + /// Slot removal for an atom whose counter already reached zero under a + /// shared borrow. + AtomRemove(Atom), ActiveFramePop { token: ActiveFrameToken, depth: usize, @@ -167,7 +174,7 @@ impl RuntimeState { pub(crate) fn apply_committed_cleanup(&mut self, cleanup: HeapCleanup) { self.unlink_finalized_shapes(cleanup.finalized_shape_ids); - self.release_atoms(cleanup.atoms) + self.release_atom_indices(cleanup.atoms) .expect("committed heap cleanup atom release failed"); } @@ -180,18 +187,30 @@ impl RuntimeState { .expect("committed pending-exception object release failed"); self.apply_committed_cleanup(cleanup); } - RawValue::Symbol(atom) => { + RawValue::Symbol(index) => { self.atoms - .release(atom) + .release_index(index) .expect("committed pending-exception Symbol release failed"); } + RawValue::String(id) => { + let cleanup = self + .heap + .release_string(id) + .expect("committed pending-exception string release failed"); + self.apply_committed_cleanup(cleanup); + } + RawValue::BigInt(id) => { + let cleanup = self + .heap + .release_bigint(id) + .expect("committed pending-exception bigint release failed"); + self.apply_committed_cleanup(cleanup); + } RawValue::Undefined | RawValue::Null | RawValue::Bool(_) | RawValue::Int(_) - | RawValue::Float(_) - | RawValue::BigInt(_) - | RawValue::String(_) => {} + | RawValue::Float(_) => {} RawValue::Private(_) | RawValue::Uninitialized | RawValue::Exception => { unreachable!("internal value occupied committed pending-exception storage") } @@ -201,9 +220,11 @@ impl RuntimeState { pub(crate) fn retain_raw_root(&mut self, value: &RawValue) -> Result<(), RuntimeError> { match value { RawValue::Object(object) => self.heap.retain_object(*object)?, - RawValue::Symbol(atom) => { - self.atoms.retain(*atom)?; + RawValue::Symbol(index) => { + self.atoms.retain_index(*index)?; } + RawValue::String(id) => self.heap.retain_string(*id)?, + RawValue::BigInt(id) => self.heap.retain_bigint(*id)?, RawValue::Private(_) => { return Err(RuntimeError::Invariant( "private-name identity cannot become a public runtime root", @@ -213,9 +234,7 @@ impl RuntimeState { | RawValue::Null | RawValue::Bool(_) | RawValue::Int(_) - | RawValue::Float(_) - | RawValue::BigInt(_) - | RawValue::String(_) => {} + | RawValue::Float(_) => {} RawValue::Uninitialized | RawValue::Exception => { return Err(RuntimeError::Invariant( "internal value sentinel cannot become a runtime root", @@ -231,8 +250,16 @@ impl RuntimeState { let cleanup = self.heap.release_object(object)?; self.apply_cleanup(cleanup)?; } - RawValue::Symbol(atom) => { - self.atoms.release(atom)?; + RawValue::Symbol(index) => { + self.atoms.release_index(index)?; + } + RawValue::String(id) => { + let cleanup = self.heap.release_string(id)?; + self.apply_cleanup(cleanup)?; + } + RawValue::BigInt(id) => { + let cleanup = self.heap.release_bigint(id)?; + self.apply_cleanup(cleanup)?; } RawValue::Private(_) => { return Err(RuntimeError::Invariant( @@ -243,9 +270,7 @@ impl RuntimeState { | RawValue::Null | RawValue::Bool(_) | RawValue::Int(_) - | RawValue::Float(_) - | RawValue::BigInt(_) - | RawValue::String(_) => {} + | RawValue::Float(_) => {} RawValue::Uninitialized | RawValue::Exception => { return Err(RuntimeError::Invariant( "internal value sentinel occupied a runtime root", @@ -358,15 +383,13 @@ impl RuntimeState { ) -> Result, RuntimeError> { let mut retained_atoms = Vec::with_capacity(entries.len()); for entry in entries { - if let Err(error) = self.atoms.resolve(entry.atom) { + // Shape entries hold unbranded indices under the retain invariant; + // the index operation itself validates the slot before counting. + if let Err(error) = self.atoms.retain_index(entry.atom) { self.release_atoms(retained_atoms)?; return Err(error.into()); } - if let Err(error) = self.atoms.retain(entry.atom) { - self.release_atoms(retained_atoms)?; - return Err(error.into()); - } - retained_atoms.push(entry.atom); + retained_atoms.push(Atom::from_raw(entry.atom.raw())); } Ok(retained_atoms) @@ -376,20 +399,22 @@ impl RuntimeState { &mut self, slots: &[PropertySlot], ) -> Result, RuntimeError> { - let atoms = slots + let indices = slots .iter() .filter_map(|slot| match slot { - PropertySlot::Data(RawValue::Symbol(atom) | RawValue::Private(atom)) => Some(*atom), + PropertySlot::Data(RawValue::Symbol(index) | RawValue::Private(index)) => { + Some(*index) + } _ => None, }) .collect::>(); - let mut retained = Vec::with_capacity(atoms.len()); - for atom in atoms { - if let Err(error) = self.atoms.retain(atom) { + let mut retained = Vec::with_capacity(indices.len()); + for index in indices { + if let Err(error) = self.atoms.retain_index(index) { self.release_atoms(retained)?; return Err(error.into()); } - retained.push(atom); + retained.push(crate::engine::atom::Atom::from_raw(index.raw())); } Ok(retained) } @@ -398,17 +423,17 @@ impl RuntimeState { &mut self, values: impl IntoIterator, ) -> Result, RuntimeError> { - let atoms = values.into_iter().filter_map(|value| match value { - RawValue::Symbol(atom) | RawValue::Private(atom) => Some(*atom), + let indices = values.into_iter().filter_map(|value| match value { + RawValue::Symbol(index) | RawValue::Private(index) => Some(*index), _ => None, }); let mut retained = Vec::new(); - for atom in atoms { - if let Err(error) = self.atoms.retain(atom) { + for index in indices { + if let Err(error) = self.atoms.retain_index(index) { self.release_atoms(retained)?; return Err(error.into()); } - retained.push(atom); + retained.push(crate::engine::atom::Atom::from_raw(index.raw())); } Ok(retained) } @@ -519,7 +544,7 @@ impl RuntimeState { pub(crate) fn apply_cleanup(&mut self, cleanup: HeapCleanup) -> Result<(), RuntimeError> { self.unlink_finalized_shapes(cleanup.finalized_shape_ids); - self.release_atoms(cleanup.atoms) + self.release_atom_indices(cleanup.atoms) } pub(crate) fn unlink_finalized_shapes(&mut self, shapes: impl IntoIterator) { @@ -534,12 +559,31 @@ impl RuntimeState { } } + /// Release a rollback list produced by the `retain_*` helpers above. + /// + /// Those lists may carry unbranded `Atom::from_raw` reconstructions (the + /// helpers retain by unbranded index), so release goes through the index + /// operation — which re-validates the slot — rather than the branded + /// public [`AtomTable::release`]. pub(crate) fn release_atoms( &mut self, atoms: impl IntoIterator, ) -> Result<(), RuntimeError> { for atom in atoms { - self.atoms.release(atom)?; + self.atoms.release_index(AtomIdx::from_raw(atom.raw()))?; + } + Ok(()) + } + + /// Release unbranded atom indices returned from heap cleanup. Each index + /// was owned by the finalized node; the table validates the slot again on + /// the way out. + pub(crate) fn release_atom_indices( + &mut self, + indices: impl IntoIterator, + ) -> Result<(), RuntimeError> { + for index in indices { + self.atoms.release_index(index)?; } Ok(()) } @@ -572,15 +616,27 @@ impl Drop for RuntimeInner { .run_gc_for_runtime_teardown() .map_err(RuntimeError::Heap) .and_then(|mut stats| { - let atoms = std::mem::take(&mut stats.cleanup.atoms); - state.release_atoms(atoms) + let atom_indices = std::mem::take(&mut stats.cleanup.atoms); + state.release_atom_indices(atom_indices) }); debug_assert!(result.is_ok(), "runtime teardown failed: {result:?}"); - debug_assert_eq!( - state.heap.counts().live, - 0, - "runtime teardown left live heap nodes" - ); + #[cfg(debug_assertions)] + { + let live = state.heap.counts().live; + if live != 0 && std::env::var_os("QJS_TEARDOWN_PROBE").is_some() { + let roots = state.heap.debug_external_roots(); + let shown = roots.len().min(6); + eprintln!( + "[teardown] thread={:?} live={live} shown={}/{} {:?}", + std::thread::current().name(), + roots.len(), + shown, + &roots[..shown] + ); + } else { + debug_assert_eq!(live, 0, "runtime teardown left live heap nodes"); + } + } } } diff --git a/src/engine/heap/runtime/tests.rs b/src/engine/heap/runtime/tests.rs index 031be00b..e6b6ce3f 100644 --- a/src/engine/heap/runtime/tests.rs +++ b/src/engine/heap/runtime/tests.rs @@ -14,6 +14,7 @@ use crate::engine::code::debug::{DebugInfoMode, Pc2LineEntry, Pc2LineTable}; use crate::engine::code::dynamic_source::DynamicSourceBuilder; use crate::source::LineColumn; +use crate::engine::atom::AtomIdx; use crate::engine::code::function::metadata::{ ClosureSource, ClosureVariable, ClosureVariableKind, ClosureVariableName, ConstructorKind, EvalKind, FunctionKind, FunctionMetadata, @@ -34,7 +35,7 @@ use crate::engine::object::{ AccessorValue, CallableRef, CompleteOrdinaryPropertyDescriptor, DescriptorField, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }; -use crate::engine::value::{JsString, JsStringError, Value}; +use crate::engine::value::{JsString, JsStringError, JsValue, Value}; use crate::engine::vm::call::CallableExecution; use crate::engine::vm::{Completion, ToPrimitiveHint}; diff --git a/src/engine/heap/runtime/tests/active_frames.rs b/src/engine/heap/runtime/tests/active_frames.rs index 257ef2ef..be84552c 100644 --- a/src/engine/heap/runtime/tests/active_frames.rs +++ b/src/engine/heap/runtime/tests/active_frames.rs @@ -71,19 +71,23 @@ fn unified_active_frames_preserve_order_caller_pc_and_defining_realms() { (outer_bytecode, *bytecode) }; + let completion = runtime + .call_internal( + outer_context.realm, + &outer, + Value::Undefined, + &[ + Value::Object(probe.as_object().clone()), + Value::Object(callback.as_object().clone()), + ], + ) + .unwrap(); + let Completion::Return(value) = completion else { + panic!("expected return completion"); + }; assert_eq!( - runtime - .call_internal( - outer_context.realm, - &outer, - Value::Undefined, - &[ - Value::Object(probe.as_object().clone()), - Value::Object(callback.as_object().clone()), - ], - ) - .unwrap(), - Completion::Return(Value::Undefined) + runtime.root_and_release_jsvalue(value).unwrap(), + Value::Undefined ); let snapshot = runtime diff --git a/src/engine/heap/runtime/tests/arrays.rs b/src/engine/heap/runtime/tests/arrays.rs index b6340484..b9dcd356 100644 --- a/src/engine/heap/runtime/tests/arrays.rs +++ b/src/engine/heap/runtime/tests/arrays.rs @@ -63,7 +63,7 @@ fn array_class_roots_length_layout_values_and_realm_prototype() { assert!( state .atoms - .array_index(shape.entries()[0].atom) + .array_index(state.atoms.brand(shape.entries()[0].atom).unwrap()) .unwrap() .is_none() ); @@ -348,16 +348,20 @@ fn array_join_separator_overflow_still_gets_nullish_slots_and_later_throw_wins() context.realm, ArrayJoinKind::Join, crate::engine::vm::call::NativeInvocation::Call { - this_value: Value::Object(source), + this_value: runtime.into_jsvalue(Value::Object(source)).unwrap(), }, &crate::engine::vm::call::NativeArguments { actual_arg_count: 1, - readable: vec![Value::String(JsString::from_static("xx"))], + readable: vec![ + runtime + .into_jsvalue(Value::String(JsString::from_static("xx"))) + .unwrap(), + ], }, 2, ) .unwrap(); - assert!(matches!(completion, Completion::Throw(Value::Int(77)))); + assert!(matches!(completion, Completion::Throw(JsValue::Int(77)))); assert_eq!( context.eval("joinOverflowLog").unwrap(), Value::String(JsString::from_static("123")) @@ -390,7 +394,7 @@ fn array_locale_separator_overflow_invokes_method_but_skips_result_to_string() { context.realm, ArrayJoinKind::ToLocaleString, crate::engine::vm::call::NativeInvocation::Call { - this_value: Value::Object(source), + this_value: runtime.into_jsvalue(Value::Object(source)).unwrap(), }, &crate::engine::vm::call::NativeArguments { actual_arg_count: 0, @@ -433,7 +437,7 @@ fn array_locale_method_throw_replaces_pending_separator_overflow() { context.realm, ArrayJoinKind::ToLocaleString, crate::engine::vm::call::NativeInvocation::Call { - this_value: Value::Object(source), + this_value: runtime.into_jsvalue(Value::Object(source)).unwrap(), }, &crate::engine::vm::call::NativeArguments { actual_arg_count: 0, @@ -442,7 +446,7 @@ fn array_locale_method_throw_replaces_pending_separator_overflow() { 2, ) .unwrap(); - assert!(matches!(completion, Completion::Throw(Value::Int(88)))); + assert!(matches!(completion, Completion::Throw(JsValue::Int(88)))); } #[test] diff --git a/src/engine/heap/runtime/tests/binary_apply.rs b/src/engine/heap/runtime/tests/binary_apply.rs index 106d0eb5..5125afc2 100644 --- a/src/engine/heap/runtime/tests/binary_apply.rs +++ b/src/engine/heap/runtime/tests/binary_apply.rs @@ -1021,7 +1021,7 @@ fn construct_only_proxy_and_new_target_do_not_require_call_capability() { &[Value::Int(42)], ) .unwrap(), - Completion::Return(Value::Object(_)) + Completion::Return(JsValue::Object(_)) )); assert_eq!( expect_string_value(context.eval("__qjo_construct_only_log").unwrap()), @@ -1294,18 +1294,21 @@ fn trusted_quickjs_ordinary_apply_raw_native_constructors_use_class_fallbacks() actual_arg_count: 0, readable: Vec::new(), }; - let Completion::Return(Value::Object(direct_array)) = runtime + let completion = runtime .call_array_constructor( context.realm, crate::engine::vm::call::NativeInvocation::Construct { - new_target: Value::Undefined, + new_target: JsValue::Undefined, }, &direct_arguments, ) - .unwrap() - else { + .unwrap(); + let Completion::Return(value) = completion else { panic!("Array undefined newTarget required an active function"); }; + let Value::Object(direct_array) = runtime.root_and_release_jsvalue(value).unwrap() else { + panic!("Array undefined newTarget required an object"); + }; assert_eq!( runtime.get_prototype_of(&direct_array).unwrap(), Some(context.array_prototype().unwrap()) diff --git a/src/engine/heap/runtime/tests/binary_publication.rs b/src/engine/heap/runtime/tests/binary_publication.rs index 5e05f53c..97d66bc8 100644 --- a/src/engine/heap/runtime/tests/binary_publication.rs +++ b/src/engine/heap/runtime/tests/binary_publication.rs @@ -272,7 +272,7 @@ fn trusted_quickjs_ordinary_leaf_synthesizes_bigint_and_canonical_empty_atom_con assert!(matches!( snapshot.constants.as_ref(), [BytecodeConstant::Value(RawValue::BigInt(value))] - if value == &JsBigInt::from(42) + if runtime.0.state.borrow().heap.bigint(*value).unwrap() == &JsBigInt::from(42) )); let direct_image = quickjs_ordinary_with_code_and_constants(&[0xbf, 0x28], &[]); @@ -361,11 +361,13 @@ fn trusted_quickjs_ordinary_leaf_synthesizes_bigint_and_canonical_empty_atom_con )); assert!(matches!( &snapshot.constants[1], - BytecodeConstant::Value(RawValue::BigInt(value)) if value == &JsBigInt::from(7) + BytecodeConstant::Value(RawValue::BigInt(value)) + if runtime.0.state.borrow().heap.bigint(*value).unwrap() == &JsBigInt::from(7) )); assert!(matches!( &snapshot.constants[3], - BytecodeConstant::Value(RawValue::BigInt(value)) if value == &JsBigInt::from(-3) + BytecodeConstant::Value(RawValue::BigInt(value)) + if runtime.0.state.borrow().heap.bigint(*value).unwrap() == &JsBigInt::from(-3) )); let BytecodeConstant::Value(RawValue::String(first_empty)) = &snapshot.constants[2] else { panic!("first synthesized empty atom lost its String payload"); @@ -373,8 +375,14 @@ fn trusted_quickjs_ordinary_leaf_synthesizes_bigint_and_canonical_empty_atom_con let BytecodeConstant::Value(RawValue::String(second_empty)) = &snapshot.constants[4] else { panic!("second synthesized empty atom lost its String payload"); }; - assert!(first_empty.same_representation(second_empty)); - assert!(mixed_result.same_representation(first_empty)); + { + let state = runtime.0.state.borrow(); + assert_eq!( + state.heap.string(*first_empty).unwrap(), + state.heap.string(*second_empty).unwrap() + ); + assert_eq!(&mixed_result, state.heap.string(*first_empty).unwrap()); + } } #[test] diff --git a/src/engine/heap/runtime/tests/binary_read_only.rs b/src/engine/heap/runtime/tests/binary_read_only.rs index 2b4ce944..2b1c32b7 100644 --- a/src/engine/heap/runtime/tests/binary_read_only.rs +++ b/src/engine/heap/runtime/tests/binary_read_only.rs @@ -30,7 +30,10 @@ fn trusted_quickjs_ordinary_read_only_uses_exact_zero_stack_wire_and_type_error( let [BytecodeConstant::Value(RawValue::String(name))] = snapshot.constants.as_ref() else { panic!("raw49 name was not published as one verified String constant"); }; - assert_eq!(name, &JsString::from_static("x")); + assert_eq!( + runtime.0.state.borrow().heap.string(*name).unwrap(), + &JsString::from_static("x") + ); drop(snapshot); assert_eq!( diff --git a/src/engine/heap/runtime/tests/closures.rs b/src/engine/heap/runtime/tests/closures.rs index a70bf6b9..bc094a75 100644 --- a/src/engine/heap/runtime/tests/closures.rs +++ b/src/engine/heap/runtime/tests/closures.rs @@ -44,7 +44,7 @@ fn function_closures_share_runtime_rooted_var_ref_cells() { .unwrap(); let function = runtime.test_child_function_bytecode(&root, 0).unwrap(); let cell = runtime - .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .new_var_ref(JsValue::Int(1), false, false, ClosureVariableKind::Normal) .unwrap(); let cell_id = cell.id(); let first = runtime @@ -68,15 +68,15 @@ fn function_closures_share_runtime_rooted_var_ref_cells() { Value::Int(3) ); - runtime.write_var_ref(&cell, Value::Int(7)).unwrap(); - assert_eq!(runtime.read_var_ref(&cell).unwrap(), Value::Int(7)); + runtime.write_var_ref(&cell, JsValue::Int(7)).unwrap(); + assert_eq!(runtime.read_var_ref(&cell).unwrap(), JsValue::Int(7)); drop(cell); assert_eq!( runtime.0.state.borrow().heap.var_ref_strong_count(cell_id), Ok(2) ); let promoted = VarRefRoot::from_borrowed_handle(runtime.clone(), cell_id).unwrap(); - assert_eq!(runtime.read_var_ref(&promoted).unwrap(), Value::Int(7)); + assert_eq!(runtime.read_var_ref(&promoted).unwrap(), JsValue::Int(7)); drop(first); drop(second); assert_eq!( diff --git a/src/engine/heap/runtime/tests/native_calls.rs b/src/engine/heap/runtime/tests/native_calls.rs index 4b004810..de097dd2 100644 --- a/src/engine/heap/runtime/tests/native_calls.rs +++ b/src/engine/heap/runtime/tests/native_calls.rs @@ -1,5 +1,19 @@ use super::*; +fn returned(runtime: &Runtime, completion: Completion) -> Value { + match completion { + Completion::Return(value) => runtime.root_and_release_jsvalue(value).unwrap(), + Completion::Throw(_) => panic!("expected return completion"), + } +} + +fn thrown(runtime: &Runtime, completion: Completion) -> Value { + match completion { + Completion::Throw(value) => runtime.root_and_release_jsvalue(value).unwrap(), + Completion::Return(_) => panic!("expected throw completion"), + } +} + #[test] fn native_function_retains_and_dispatches_in_its_defining_realm() { let runtime = Runtime::new(); @@ -90,8 +104,8 @@ fn native_call_preserves_actual_argc_padding_and_restores_active_frame() { .call_internal(caller_context.realm, &probe, Value::Undefined, &[]) .unwrap(); assert_eq!( - no_args, - Completion::Return(Value::String(JsString::from_static("0|2|2|false"))) + returned(&runtime, no_args), + Value::String(JsString::from_static("0|2|2|false")) ); let extra_args = runtime .call_internal( @@ -102,21 +116,24 @@ fn native_call_preserves_actual_argc_padding_and_restores_active_frame() { ) .unwrap(); assert_eq!( - extra_args, - Completion::Return(Value::String(JsString::from_static("3|3|0|false"))) + returned(&runtime, extra_args), + Value::String(JsString::from_static("3|3|0|false")) ); assert!(runtime.0.state.borrow().active_frames.is_empty()); assert_eq!( - runtime - .call_internal( - caller_context.realm, - &probe, - Value::Undefined, - &[Value::Bool(false)], - ) - .unwrap(), - Completion::Throw(Value::String(JsString::from_static("native probe throw"))) + thrown( + &runtime, + runtime + .call_internal( + caller_context.realm, + &probe, + Value::Undefined, + &[Value::Bool(false)], + ) + .unwrap(), + ), + Value::String(JsString::from_static("native probe throw")) ); assert!(runtime.0.state.borrow().active_frames.is_empty()); @@ -233,7 +250,10 @@ fn native_constructor_cproto_adapters_use_defining_realm_and_restore_frames() { &[], ) .unwrap(); - let Completion::Throw(Value::Object(exception)) = called_without_new else { + let Completion::Throw(value) = called_without_new else { + panic!("constructor-only native did not throw an object"); + }; + let Value::Object(exception) = runtime.root_and_release_jsvalue(value).unwrap() else { panic!("constructor-only native did not throw an object"); }; let defining_type_error_prototype = runtime @@ -264,37 +284,46 @@ fn native_constructor_cproto_adapters_use_defining_realm_and_restore_frames() { assert!(runtime.0.state.borrow().active_frames.is_empty()); assert_eq!( - runtime - .construct_internal( - caller_context.realm, - &constructor_only, - &constructor_only, - &[], - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("0|0|0|true"))) + returned( + &runtime, + runtime + .construct_internal( + caller_context.realm, + &constructor_only, + &constructor_only, + &[], + ) + .unwrap(), + ), + Value::String(JsString::from_static("0|0|0|true")) ); assert_eq!( - runtime - .call_internal( - caller_context.realm, - &constructor_or_function, - Value::Undefined, - &[], - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("0|0|0|false"))) + returned( + &runtime, + runtime + .call_internal( + caller_context.realm, + &constructor_or_function, + Value::Undefined, + &[], + ) + .unwrap(), + ), + Value::String(JsString::from_static("0|0|0|false")) ); assert_eq!( - runtime - .construct_internal( - caller_context.realm, - &constructor_or_function, - &constructor_or_function, - &[], - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("0|0|0|true"))) + returned( + &runtime, + runtime + .construct_internal( + caller_context.realm, + &constructor_or_function, + &constructor_or_function, + &[], + ) + .unwrap(), + ), + Value::String(JsString::from_static("0|0|0|true")) ); assert!(runtime.0.state.borrow().active_frames.is_empty()); } diff --git a/src/engine/heap/runtime/tests/shapes.rs b/src/engine/heap/runtime/tests/shapes.rs index d0c22e1f..98ca30ab 100644 --- a/src/engine/heap/runtime/tests/shapes.rs +++ b/src/engine/heap/runtime/tests/shapes.rs @@ -76,7 +76,7 @@ fn unique_shape_append_never_mutates_a_shared_shape() { .unwrap() .entries() .iter() - .map(|entry| entry.atom) + .map(|entry| state.atoms.brand(entry.atom).unwrap()) .collect::>(), shared_keys .iter() @@ -95,7 +95,7 @@ fn unique_shape_append_never_mutates_a_shared_shape() { .unwrap() .entries() .iter() - .map(|entry| entry.atom) + .map(|entry| state.atoms.brand(entry.atom).unwrap()) .collect::>(), unique_atoms ); @@ -220,7 +220,7 @@ fn append_edges_are_weak_and_unlinked_on_mutation_and_collection() { let atom = state.atoms.intern_static("transition-key").unwrap(); let parent = state.get_or_create_shape(None, &[]).unwrap(); let entry = crate::engine::object::shape::ShapeEntry { - atom, + atom: AtomIdx::from_raw(atom.raw()), flags: crate::engine::object::shape::PropertyFlags::data(true, true, true), }; let first = state.append_transition(parent, entry).unwrap(); diff --git a/src/engine/heap/runtime/tests/strings.rs b/src/engine/heap/runtime/tests/strings.rs index 9f39270b..fb0ed0e1 100644 --- a/src/engine/heap/runtime/tests/strings.rs +++ b/src/engine/heap/runtime/tests/strings.rs @@ -798,15 +798,19 @@ fn string_conversion_core_brand_lookup_object_routes_and_overrides_match_quickjs ) .unwrap() ); + let completion = runtime + .to_primitive( + context.realm, + Value::Object(conversion_wrapper.clone()), + ToPrimitiveHint::String, + ) + .unwrap(); + let Completion::Return(value) = completion else { + panic!("expected return completion"); + }; assert_eq!( - runtime - .to_primitive( - context.realm, - Value::Object(conversion_wrapper.clone()), - ToPrimitiveHint::String, - ) - .unwrap(), - Completion::Return(Value::String(JsString::from_static("override"))) + runtime.root_and_release_jsvalue(value).unwrap(), + Value::String(JsString::from_static("override")) ); assert_eq!( context diff --git a/src/engine/heap/runtime_gc.rs b/src/engine/heap/runtime_gc.rs index 4381fc85..465e1248 100644 --- a/src/engine/heap/runtime_gc.rs +++ b/src/engine/heap/runtime_gc.rs @@ -5,7 +5,7 @@ use crate::engine::heap::runtime::RuntimeState; use crate::engine::heap::{GcStats, HeapCounts, WeakSymbolGcEvent}; use crate::engine::jobs; #[cfg(feature = "test262-host")] -use crate::engine::value::Value; +use crate::engine::value::JsValue; #[cfg(feature = "test262-host")] use crate::engine::vm::Completion; #[cfg(feature = "test262-host")] @@ -28,9 +28,9 @@ impl Runtime { heap.run_gc_with_finalization_sink( |event| { Ok(match event { - WeakSymbolGcEvent::IsLive(atom) => atoms.is_live(atom), - WeakSymbolGcEvent::Release(atom) => { - if let Err(error) = atoms.release(atom) { + WeakSymbolGcEvent::IsLive(index) => atoms.is_live_index(index), + WeakSymbolGcEvent::Release(index) => { + if let Err(error) = atoms.release_index(index) { // A detached weak value owned this atom, so this // can fail only after an ownership invariant has // already been violated. Latch the exact error but @@ -47,9 +47,9 @@ impl Runtime { if let Some(error) = atom_error { return Err(error.into()); } - let atoms = std::mem::take(&mut stats.cleanup.atoms); + let atom_indices = std::mem::take(&mut stats.cleanup.atoms); state.unlink_finalized_shapes(stats.cleanup.finalized_shape_ids.iter().copied()); - state.release_atoms(atoms)?; + state.release_atom_indices(atom_indices)?; state.atoms.sweep_released_strings(); Ok(stats) } @@ -71,7 +71,7 @@ impl Runtime { )); }; self.run_gc()?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } /// Runtime heap population for diagnostics and lifecycle tests. diff --git a/src/engine/heap/slot_ownership.rs b/src/engine/heap/slot_ownership.rs index 81f0852b..f3193ce1 100644 --- a/src/engine/heap/slot_ownership.rs +++ b/src/engine/heap/slot_ownership.rs @@ -6,7 +6,7 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; use crate::engine::heap::{Heap, HeapError, RawId, SlotState}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SlotReleaseReadiness { @@ -26,14 +26,39 @@ impl Heap { self.slot_release_readiness(RawId::Object(object)) } + /// Trusted hot-path release readiness for a live object held by an owning + /// root. Identical to [`Heap::slot_object_release_readiness`] except that + /// the generation check is omitted; a non-live slot still reports `Drain` + /// rather than aborting. + #[inline] + pub(crate) fn slot_object_release_readiness_fast( + &self, + object: super::ObjectId, + ) -> SlotReleaseReadiness { + if !self.zero_queue.is_empty() { + return SlotReleaseReadiness::Drain; + } + match &self.slots[object.index as usize].state { + SlotState::Live(node) if node.strong.get() > 1 => SlotReleaseReadiness::Ready, + SlotState::Live(node) if node.strong.get() == 1 => { + if self.zero_queue.len() == self.zero_queue.capacity() { + SlotReleaseReadiness::QueueCapacity + } else { + SlotReleaseReadiness::Drain + } + } + _ => SlotReleaseReadiness::Drain, + } + } + fn slot_release_readiness(&self, id: RawId) -> Result { let index = self.validate_slot_identity(id)?; if !self.zero_queue.is_empty() { return Ok(SlotReleaseReadiness::Drain); } match &self.slots[index].state { - SlotState::Live(node) if node.strong > 1 => Ok(SlotReleaseReadiness::Ready), - SlotState::Live(node) if node.strong == 1 => { + SlotState::Live(node) if node.strong.get() > 1 => Ok(SlotReleaseReadiness::Ready), + SlotState::Live(node) if node.strong.get() == 1 => { // release_raw_no_drain would push to this queue. Do not commit // its decrement before deciding whether that push can allocate. Ok(if self.zero_queue.len() == self.zero_queue.capacity() { @@ -50,6 +75,7 @@ impl Heap { } impl Runtime { + #[cfg_attr(not(test), allow(dead_code))] pub(crate) fn slot_value_release_readiness( &self, value: &Value, @@ -101,10 +127,48 @@ impl Runtime { } } + /// Internal-value form of [`Runtime::slot_value_release_readiness`]. + /// Handles carry no runtime branding, so the domain checks disappear; + /// every heap-backed kind reports its node or atom slot readiness. + pub(crate) fn slot_value_release_readiness_jsvalue( + &self, + value: &JsValue, + ) -> Result { + match value { + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) => return Ok(SlotReleaseReadiness::Ready), + JsValue::Object(_) | JsValue::Symbol(_) | JsValue::String(_) | JsValue::BigInt(_) => {} + } + if self.0.deferred_references.has_pending() { + return Ok(SlotReleaseReadiness::Deferred); + } + let Ok(state) = self.0.state.try_borrow_mut() else { + return Ok(SlotReleaseReadiness::Borrowed); + }; + match value { + JsValue::Object(id) => Ok(state.heap.slot_release_readiness(RawId::Object(*id))?), + JsValue::String(id) => Ok(state.heap.slot_release_readiness(RawId::String(*id))?), + JsValue::BigInt(id) => Ok(state.heap.slot_release_readiness(RawId::BigInt(*id))?), + JsValue::Symbol(index) => { + let atom = state.atoms.brand(*index)?; + Ok(match state.atoms.resolve(atom)?.ref_count { + None => SlotReleaseReadiness::Ready, + Some(count) if count > 1 => SlotReleaseReadiness::Ready, + Some(_) => SlotReleaseReadiness::PrimitiveStorage, + }) + } + _ => unreachable!("primitive slots returned before borrowing runtime state"), + } + } + /// Commit exactly one ordinary owning-root release after the no-drain /// proof. No callback or reference decrease can intervene between the /// preflight and Drop. Ready consumes the Value; every other outcome leaves /// it untouched, so the caller may move it to a pending operation safely. + #[cfg_attr(not(test), allow(dead_code))] pub(crate) fn try_release_slot_value(&self, value: &mut Value) -> Result { if self.slot_value_release_readiness(value)? != SlotReleaseReadiness::Ready { return Ok(false); @@ -119,6 +183,26 @@ impl Runtime { drop(old); Ok(true) } + + /// Internal-value form of [`Runtime::try_release_slot_value`]: on `Ready` + /// the value is replaced with `undefined` and its edges are released. + pub(crate) fn try_release_slot_value_jsvalue( + &self, + value: &mut JsValue, + ) -> Result { + if self.slot_value_release_readiness_jsvalue(value)? != SlotReleaseReadiness::Ready { + return Ok(false); + } + #[cfg(feature = "profiling")] + crate::engine::api::profiling::record_owned_storage( + crate::engine::api::profiling::OwnedStorageEvent::HotRelease { + heap_root: matches!(value, JsValue::Object(_) | JsValue::Symbol(_)), + }, + ); + let old = std::mem::replace(value, JsValue::Undefined); + self.release_jsvalue(old)?; + Ok(true) + } } #[cfg(test)] @@ -255,7 +339,9 @@ mod tests { use crate::engine::code::bytecode::Instruction; let runtime = Runtime::new(); let mut context = runtime.new_context(); - let base = context.eval("globalThis.fieldProbe={x:7}").unwrap(); + let base = runtime + .into_jsvalue(context.eval("globalThis.fieldProbe={x:7}").unwrap()) + .unwrap(); let callable = runtime .callable_from_value(context.eval("(function(o,v){o.x=v;return o.x})").unwrap()) .unwrap(); @@ -289,15 +375,16 @@ mod tests { .try_ordinary_field_immediate_read(&base, &code, key) .is_none() ); - assert!(!runtime.try_ordinary_field_immediate_write(&base, &code, key, &Value::Int(17))); + assert!(!runtime.try_ordinary_field_immediate_write(&base, &code, key, &JsValue::Int(17))); assert_eq!(runtime.0.state.borrow().heap.zero_queue.len(), 1); runtime.run_gc().unwrap(); assert_eq!(context.eval("fieldProbe.x").unwrap(), Value::Int(7)); - assert!(runtime.try_ordinary_field_immediate_write(&base, &code, key, &Value::Int(17))); + assert!(runtime.try_ordinary_field_immediate_write(&base, &code, key, &JsValue::Int(17))); assert_eq!( runtime.try_ordinary_field_immediate_read(&base, &code, key), - Some(Value::Int(17)) + Some(JsValue::Int(17)) ); + runtime.release_jsvalue(base).unwrap(); } #[test] @@ -327,11 +414,19 @@ mod tests { .release_raw_no_drain(RawId::Object(queued_id)) .unwrap(); assert_eq!(runtime.0.state.borrow().heap.zero_queue.len(), 1); - assert!(!runtime.try_typed_array_number_write(&typed, 0, 17.0)); - assert!(runtime.try_dense_array_immediate_read(&dense, 0).is_none()); - assert!(runtime.try_array_immediate_read(&dense, 0).is_none()); - assert!(runtime.try_array_immediate_read(&typed, 0).is_none()); + let dense_js = runtime.unroot_value(&dense).unwrap(); + let typed_js = runtime.unroot_value(&typed).unwrap(); + assert!(!runtime.try_typed_array_number_write(&typed_js, 0, 17.0)); + assert!( + runtime + .try_dense_array_immediate_read(&dense_js, 0) + .is_none() + ); + assert!(runtime.try_array_immediate_read(&dense_js, 0).is_none()); + assert!(runtime.try_array_immediate_read(&typed_js, 0).is_none()); assert_eq!(runtime.0.state.borrow().heap.zero_queue.len(), 1); + runtime.release_jsvalue(dense_js).unwrap(); + runtime.release_jsvalue(typed_js).unwrap(); for value in [&dense, &typed] { let Value::Object(object) = value else { panic!("array receiver"); @@ -387,7 +482,8 @@ mod tests { .heap .live_node_mut(id) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); let result = root.try_clone(); let count = runtime.0.state.borrow().heap.strong_count(id).unwrap(); // Restore the actual owner count before assertions can unwind roots. @@ -398,7 +494,8 @@ mod tests { .heap .live_node_mut(id) .unwrap() - .strong = 1; + .strong + .set(1); assert!(result.is_err()); assert_eq!(count, u32::MAX); drop(root); diff --git a/src/engine/heap/suspension_records.rs b/src/engine/heap/suspension_records.rs index b3543880..ca999b61 100644 --- a/src/engine/heap/suspension_records.rs +++ b/src/engine/heap/suspension_records.rs @@ -1,7 +1,7 @@ use super::*; /// ECMAScript-visible lifecycle of a branded synchronous generator object. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq)] pub enum GeneratorState { SuspendedStart, SuspendedYield, @@ -13,7 +13,7 @@ pub enum GeneratorState { /// Heap-native representation of one argument or local binding retained by a /// dormant generator frame. Runtime-owning root wrappers must never enter this /// structure: every GC identity is stored as a raw arena edge instead. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum GeneratorFrameBinding { Direct(RawValue), Private(Atom), @@ -23,7 +23,7 @@ pub enum GeneratorFrameBinding { } /// Raw VM fields retained across a synchronous-generator suspension. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct GeneratorVmActivation { pub stack: Vec, pub regions: Vec, @@ -38,7 +38,7 @@ pub struct GeneratorVmActivation { } /// Complete dormant execution state owned by one generator object. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct GeneratorActivationData { pub bytecode: FunctionBytecodeId, pub vm: GeneratorVmActivation, @@ -51,7 +51,7 @@ pub struct GeneratorActivationData { } /// ECMAScript-visible lifecycle of a branded async-generator object. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq)] pub enum AsyncGeneratorState { SuspendedStart, SuspendedYield, @@ -63,7 +63,7 @@ pub enum AsyncGeneratorState { /// One queued `.next`, `.return`, or `.throw` request and its Promise /// capability. Every identity is stored as a raw traced edge. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct AsyncGeneratorRequestData { pub completion: GeneratorResumeKind, pub result: RawValue, @@ -73,7 +73,7 @@ pub struct AsyncGeneratorRequestData { } /// Complete hidden state of one genuine AsyncGenerator. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct AsyncGeneratorData { pub state: AsyncGeneratorState, pub activation: Option>, @@ -84,14 +84,14 @@ pub struct AsyncGeneratorData { } /// Settlement branch selected by an internal async-function resume callback. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum AsyncFunctionResumeKind { Fulfill, Reject, } /// Settlement branch selected by an internal async-generator reaction. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum AsyncGeneratorResumeKind { AwaitFulfill, AwaitReject, @@ -104,7 +104,7 @@ pub enum AsyncGeneratorResumeKind { /// The active VM frame is rooted by the runtime while `Executing`. At an /// `await`, ownership transfers into the state object and the phase becomes /// `Awaiting`; `Completed` is absorbing. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Hash, PartialEq)] pub enum AsyncFunctionPhase { Executing, Awaiting, @@ -118,7 +118,7 @@ pub enum AsyncFunctionPhase { /// properties. `driver_realm` is the original caller realm which supplies the /// returned Promise and await jobs; it may differ from the bytecode activation /// realm. Every arena identity here is a raw, traced edge. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct AsyncFunctionStateData { pub driver_realm: ContextId, pub outer_resolve: ObjectId, @@ -537,10 +537,12 @@ pub(in crate::engine::heap) fn validate_async_from_sync_iterator_data( )); } for edge in raw_value_edges(&data.next) { - let RawId::Object(object) = edge else { - unreachable!("RawValue only owns object edges") - }; - heap.object(object)?; + if !heap.is_live(edge) { + return Err(HeapError::Stale { + index: edge.index(), + generation: edge.generation(), + }); + } } Ok(()) } diff --git a/src/engine/heap/tests.rs b/src/engine/heap/tests.rs index c6d92b0d..233b8415 100644 --- a/src/engine/heap/tests.rs +++ b/src/engine/heap/tests.rs @@ -39,24 +39,24 @@ fn small_edge_transactions_preflight_duplicates_and_late_failure() { let second = RawId::Shape(second); heap.retain_edges_transactionally(&[]).unwrap(); heap.retain_edges_transactionally(&[first]).unwrap(); - assert_eq!(heap.live_node(first).unwrap().strong, 2); + assert_eq!(heap.live_node(first).unwrap().strong.get(), 2); heap.retain_edges_transactionally(&[first, first]).unwrap(); - assert_eq!(heap.live_node(first).unwrap().strong, 4); - heap.live_node_mut(second).unwrap().strong = u32::MAX; + assert_eq!(heap.live_node(first).unwrap().strong.get(), 4); + heap.live_node_mut(second).unwrap().strong.set(u32::MAX); assert!(heap.retain_edges_transactionally(&[first, second]).is_err()); assert_eq!( - heap.live_node(first).unwrap().strong, + heap.live_node(first).unwrap().strong.get(), 4, "later edge failure must not retain the first" ); - heap.live_node_mut(first).unwrap().strong = u32::MAX - 1; + heap.live_node_mut(first).unwrap().strong.set(u32::MAX - 1); assert!(heap.retain_edges_transactionally(&[first, first]).is_err()); - assert_eq!(heap.live_node(first).unwrap().strong, u32::MAX - 1); - heap.live_node_mut(first).unwrap().strong = 1; - heap.live_node_mut(second).unwrap().strong = 1; + assert_eq!(heap.live_node(first).unwrap().strong.get(), u32::MAX - 1); + heap.live_node_mut(first).unwrap().strong.set(1); + heap.live_node_mut(second).unwrap().strong.set(1); heap.retain_edges_transactionally(&[first, second]).unwrap(); - assert_eq!(heap.live_node(first).unwrap().strong, 2); - assert_eq!(heap.live_node(second).unwrap().strong, 2); + assert_eq!(heap.live_node(first).unwrap().strong.get(), 2); + assert_eq!(heap.live_node(second).unwrap().strong.get(), 2); } #[derive(Default)] @@ -140,7 +140,7 @@ fn one_slot_shape(heap: &mut Heap) -> ShapeId { Shape::new( None, [ShapeEntry { - atom, + atom: AtomIdx::from_raw(atom.raw()), flags: DATA_FLAGS, }], ) diff --git a/src/engine/heap/tests/buffers.rs b/src/engine/heap/tests/buffers.rs index 5e584000..013b77e5 100644 --- a/src/engine/heap/tests/buffers.rs +++ b/src/engine/heap/tests/buffers.rs @@ -813,14 +813,14 @@ fn data_view_intrinsics_attach_transactionally_once() { )), ); assert_eq!(heap.object_strong_count(object_prototype), Ok(root_strong),); - assert_eq!(heap.context(realm).unwrap().data_view, None); + assert!(matches!(heap.context(realm).unwrap().data_view, None)); let prototype_strong = heap.object_strong_count(prototype).unwrap(); heap.attach_data_view_intrinsics(realm, constructor, DataViewRealmData { prototype }) .unwrap(); - assert_eq!( - heap.context(realm).unwrap().data_view, - Some(DataViewRealmData { prototype }), + assert!( + matches!(heap.context(realm).unwrap().data_view, Some(attached) + if attached.prototype == prototype) ); assert_eq!( heap.object_strong_count(prototype), @@ -850,8 +850,8 @@ fn data_view_intrinsics_attach_transactionally_once() { fn symbol_atom_ownership_is_returned_on_replace_and_finalize() { let mut heap = Heap::new(); let shape = one_slot_shape(&mut heap); - let first_symbol = Atom::from_raw(17); - let second_symbol = Atom::from_raw(23); + let first_symbol = AtomIdx::from_raw(17); + let second_symbol = AtomIdx::from_raw(23); let object = heap .allocate_object(ObjectData::ordinary( shape, @@ -873,6 +873,8 @@ fn symbol_atom_ownership_is_returned_on_replace_and_finalize() { let shape_cleanup = heap.release_shape(shape).unwrap(); assert_eq!( shape_cleanup.atoms, - vec![Atom::from_immediate_integer(0).unwrap()] + vec![AtomIdx::from_raw( + Atom::from_immediate_integer(0).unwrap().raw() + )] ); } diff --git a/src/engine/heap/tests/bytecode.rs b/src/engine/heap/tests/bytecode.rs index a688a75f..22499e37 100644 --- a/src/engine/heap/tests/bytecode.rs +++ b/src/engine/heap/tests/bytecode.rs @@ -39,7 +39,7 @@ fn bytecode_debug_filename_requires_one_auxiliary_atom_ownership() { let bytecode = heap.allocate_function_bytecode(owned).unwrap(); assert_eq!( heap.release_function_bytecode(bytecode).unwrap().atoms, - vec![filename] + vec![AtomIdx::from_raw(filename.raw())] ); } @@ -638,12 +638,13 @@ fn bytecode_allocation_accepts_typed_class_heritage() { Instruction::Drop, Instruction::Return, ]); + let derived = heap + .allocate_string(JsString::from_static("Derived")) + .unwrap(); let mut candidate = bytecode( &code, context, - vec![BytecodeConstant::Value(RawValue::String( - JsString::from_static("Derived"), - ))], + vec![BytecodeConstant::Value(RawValue::String(derived))], Vec::new(), ); let mut atoms = crate::engine::atom::AtomTable::new(); @@ -653,6 +654,7 @@ fn bytecode_allocation_accepts_typed_class_heritage() { candidate.metadata.max_stack = 2; let function = heap.allocate_function_bytecode(candidate).unwrap(); heap.release_function_bytecode(function).unwrap(); + heap.release_string(derived).unwrap(); heap.release_context(context).unwrap(); heap.release_shape(shape).unwrap(); @@ -784,17 +786,26 @@ fn bytecode_static_names_require_complete_owned_string_mappings() { data.property_key_atoms = keys; data }; - let spelling = || RawValue::String(JsString::from_static("mapped")); + let mut spellings: Vec = Vec::new(); + let mut spelling = |heap: &mut Heap| { + let id = heap + .allocate_string(JsString::from_static("mapped")) + .unwrap(); + spellings.push(id); + RawValue::String(id) + }; for keys in [None, Some(Rc::from([Atom::NULL]))] { + let value = spelling(&mut heap); assert_eq!( - heap.allocate_function_bytecode(candidate(keys, vec![], spelling())), + heap.allocate_function_bytecode(candidate(keys, vec![], value)), Err(HeapError::Invariant( "static name opcode has no linked property key" )) ); } + let unowned = spelling(&mut heap); assert_eq!( - heap.allocate_function_bytecode(candidate(Some(Rc::from([name])), vec![], spelling())), + heap.allocate_function_bytecode(candidate(Some(Rc::from([name])), vec![], unowned)), Err(HeapError::Invariant( "static name atom is not owned by bytecode metadata" )) @@ -809,13 +820,17 @@ fn bytecode_static_names_require_complete_owned_string_mappings() { "linked property key does not reference a string constant" )) ); + let linked = spelling(&mut heap); let id = heap - .allocate_function_bytecode(candidate(Some(Rc::from([name])), vec![name], spelling())) + .allocate_function_bytecode(candidate(Some(Rc::from([name])), vec![name], linked)) .unwrap(); assert_eq!( heap.release_function_bytecode(id).unwrap().atoms, - vec![name] + vec![AtomIdx::from_raw(name.raw())] ); + for id in spellings { + heap.release_string(id).unwrap(); + } heap.release_context(realm).unwrap(); heap.release_object(prototype).unwrap(); heap.release_shape(shape).unwrap(); diff --git a/src/engine/heap/tests/collections.rs b/src/engine/heap/tests/collections.rs index 4e029bac..e248368d 100644 --- a/src/engine/heap/tests/collections.rs +++ b/src/engine/heap/tests/collections.rs @@ -1,7 +1,14 @@ use crate::engine::heap::native::{MapNativeKind, NativeCProto, SetNativeKind}; +use crate::engine::value::collection_key; use super::*; +/// Intern an immediate symbol atom into the unbranded index form stored in +/// `RawValue::Symbol`. +fn symbol_index(value: u32) -> AtomIdx { + AtomIdx::from_raw(Atom::from_immediate_integer(value).unwrap().raw()) +} + #[test] fn collection_churn_reclaims_records_with_a_paused_iterator() { let mut heap = Heap::new(); @@ -152,12 +159,9 @@ fn map_record_ids_preserve_readd_order_and_live_iterator_sees_appends() { let map = heap .allocate_object(ObjectData::map(shape, Vec::new())) .unwrap(); - heap.map_insert_record( - map, - RawValue::Int(1), - RawValue::String(JsString::from_static("one")), - ) - .unwrap(); + let one = heap.allocate_string(JsString::from_static("one")).unwrap(); + heap.map_insert_record(map, RawValue::Int(1), RawValue::String(one)) + .unwrap(); let iterator = heap .allocate_object(ObjectData::map_iterator( shape, @@ -168,43 +172,46 @@ fn map_record_ids_preserve_readd_order_and_live_iterator_sees_appends() { .unwrap(); heap.set_map_iterator_index(iterator, 1).unwrap(); - heap.map_insert_record( - map, - RawValue::Int(2), - RawValue::String(JsString::from_static("first")), - ) - .unwrap(); - assert_eq!( + let first = heap + .allocate_string(JsString::from_static("first")) + .unwrap(); + heap.map_insert_record(map, RawValue::Int(2), RawValue::String(first)) + .unwrap(); + assert!(matches!( heap.map_records(map).unwrap().get(1).unwrap().key, RawValue::Int(2) - ); + )); heap.map_delete_record(map, 1).unwrap(); - heap.map_insert_record( - map, - RawValue::Int(2), - RawValue::String(JsString::from_static("second")), - ) - .unwrap(); + let second = heap + .allocate_string(JsString::from_static("second")) + .unwrap(); + heap.map_insert_record(map, RawValue::Int(2), RawValue::String(second)) + .unwrap(); let records = heap.map_records(map).unwrap(); assert_eq!(records.len(), 2); assert_eq!(records.next_id(), 3); - assert_eq!(records.get(0).unwrap().key, RawValue::Int(1)); + assert!(matches!(records.get(0).unwrap().key, RawValue::Int(1))); assert!(records.get(1).is_none()); - assert_eq!(records.get(2).unwrap().key, RawValue::Int(2)); - assert_eq!( - records.get(2).unwrap().value, - RawValue::String(JsString::from_static("second")) + assert!(matches!(records.get(2).unwrap().key, RawValue::Int(2))); + assert!( + collection_key::same_value_zero( + &heap, + &records.get(2).unwrap().value, + &RawValue::String(second), + ), + "record value mismatch: {:?}", + records.get(2).unwrap().value ); let (source, next_index, kind) = heap.map_iterator_state(iterator).unwrap(); assert_eq!(source, Some(map)); assert_eq!(next_index, 1); assert_eq!(kind, MapIteratorKind::KeyAndValue); - assert_eq!( + assert!( records .next_at_or_after(next_index) - .map(|(_, record)| &record.key), - Some(&RawValue::Int(2)) + .map(|(_, record)| &record.key) + .is_some_and(|key| matches!(key, RawValue::Int(2))) ); assert_eq!(heap.object_strong_count(map), Ok(2)); @@ -224,6 +231,9 @@ fn map_record_ids_preserve_readd_order_and_live_iterator_sees_appends() { heap.release_object(iterator).unwrap(); heap.release_shape(shape).unwrap(); + for id in [one, first, second] { + heap.release_string(id).unwrap(); + } assert_eq!(heap.counts().live, 0); } @@ -234,11 +244,11 @@ fn map_symbol_atoms_transfer_and_return_on_replace_delete_and_clear() { let map = heap .allocate_object(ObjectData::map(shape, Vec::new())) .unwrap(); - let first_key = Atom::from_immediate_integer(101).unwrap(); - let first_value = Atom::from_immediate_integer(102).unwrap(); - let replacement = Atom::from_immediate_integer(103).unwrap(); - let second_key = Atom::from_immediate_integer(104).unwrap(); - let second_value = Atom::from_immediate_integer(105).unwrap(); + let first_key = symbol_index(101); + let first_value = symbol_index(102); + let replacement = symbol_index(103); + let second_key = symbol_index(104); + let second_value = symbol_index(105); heap.map_insert_record( map, @@ -306,14 +316,15 @@ fn map_intrinsics_attach_transactionally_and_root_the_realm_graph() { heap.live_node_mut(RawId::Object(iterator_prototype)) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); assert_eq!( heap.attach_map_intrinsics(realm, map), Err(HeapError::Overflow { operation: "retaining outgoing heap edges", }) ); - assert_eq!(heap.context(realm).unwrap().map, None); + assert!(matches!(heap.context(realm).unwrap().map, None)); assert_eq!(heap.object_strong_count(prototype), Ok(prototype_strong)); assert_eq!( heap.object_strong_count(constructor), @@ -321,10 +332,13 @@ fn map_intrinsics_attach_transactionally_and_root_the_realm_graph() { ); heap.live_node_mut(RawId::Object(iterator_prototype)) .unwrap() - .strong = iterator_strong; + .strong + .set(iterator_strong); heap.attach_map_intrinsics(realm, map).unwrap(); - assert_eq!(heap.context(realm).unwrap().map, Some(map)); + assert!(matches!(heap.context(realm).unwrap().map, Some(attached) + if attached.prototype == map.prototype + && attached.iterator_prototype == map.iterator_prototype)); assert_eq!( heap.object_strong_count(prototype), Ok(prototype_strong + 1) @@ -414,17 +428,21 @@ fn set_records_retain_key_edges_and_release_deleted_storage() { ); assert_eq!(heap.set_size(set), Ok(1)); assert_eq!(heap.object_strong_count(key), Ok(2)); - assert_eq!( - heap.set_records(set) - .unwrap() - .iter() - .cloned() - .collect::>(), - vec![MapRecord { - key: RawValue::Object(key), - value: RawValue::Undefined, - }] - ); + { + let mut found = heap.set_records(set).unwrap().iter(); + let record = found.next().expect("one live Set record"); + assert!(found.next().is_none()); + assert!( + matches!(record.key, RawValue::Object(object) if object == key), + "set record key mismatch: {:?}", + record.key + ); + assert!( + matches!(record.value, RawValue::Undefined), + "set record value mismatch: {:?}", + record.value + ); + } heap.release_object(key).unwrap(); let cleanup = heap.set_delete_record(set, 0).unwrap(); @@ -491,9 +509,9 @@ fn set_record_ids_preserve_readd_order_and_live_iterator_sees_appends() { let records = heap.set_records(set).unwrap(); assert_eq!(records.len(), 2); assert_eq!(records.next_id(), 3); - assert_eq!(records.get(0).unwrap().key, RawValue::Int(1)); + assert!(matches!(records.get(0).unwrap().key, RawValue::Int(1))); assert!(records.get(1).is_none()); - assert_eq!(records.get(2).unwrap().key, RawValue::Int(2)); + assert!(matches!(records.get(2).unwrap().key, RawValue::Int(2))); assert!( records .iter() @@ -504,9 +522,11 @@ fn set_record_ids_preserve_readd_order_and_live_iterator_sees_appends() { heap.set_iterator_state(iterator), Ok((Some(set), 1, SetIteratorKind::KeyAndValue)) ); - assert_eq!( - records.next_at_or_after(1).map(|(_, record)| &record.key), - Some(&RawValue::Int(2)) + assert!( + records + .next_at_or_after(1) + .map(|(_, record)| &record.key) + .is_some_and(|key| matches!(key, RawValue::Int(2))) ); assert_eq!(heap.object_strong_count(set), Ok(2)); @@ -540,9 +560,9 @@ fn set_symbol_atoms_transfer_and_return_on_delete_clear_and_finalize() { let set = heap .allocate_object(ObjectData::set(shape, Vec::new())) .unwrap(); - let first = Atom::from_immediate_integer(201).unwrap(); - let second = Atom::from_immediate_integer(202).unwrap(); - let third = Atom::from_immediate_integer(203).unwrap(); + let first = symbol_index(201); + let second = symbol_index(202); + let third = symbol_index(203); heap.set_insert_record(set, RawValue::Symbol(first)) .unwrap(); @@ -581,10 +601,13 @@ fn set_layout_and_iterator_source_are_structurally_validated() { payload: ObjectPayload::Set { records: { let mut records = CollectionRecords::default(); - records.insert(MapRecord { - key: RawValue::Int(1), - value: RawValue::Int(2), - }); + records.insert( + &heap, + MapRecord { + key: RawValue::Int(1), + value: RawValue::Int(2), + }, + ); records }, }, @@ -665,14 +688,15 @@ fn set_intrinsics_attach_transactionally_and_root_the_realm_graph() { heap.live_node_mut(RawId::Object(iterator_prototype)) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); assert_eq!( heap.attach_set_intrinsics(realm, set), Err(HeapError::Overflow { operation: "retaining outgoing heap edges", }) ); - assert_eq!(heap.context(realm).unwrap().set, None); + assert!(matches!(heap.context(realm).unwrap().set, None)); assert_eq!(heap.object_strong_count(prototype), Ok(prototype_strong)); assert_eq!( heap.object_strong_count(constructor), @@ -680,10 +704,13 @@ fn set_intrinsics_attach_transactionally_and_root_the_realm_graph() { ); heap.live_node_mut(RawId::Object(iterator_prototype)) .unwrap() - .strong = iterator_strong; + .strong + .set(iterator_strong); heap.attach_set_intrinsics(realm, set).unwrap(); - assert_eq!(heap.context(realm).unwrap().set, Some(set)); + assert!(matches!(heap.context(realm).unwrap().set, Some(attached) + if attached.prototype == set.prototype + && attached.iterator_prototype == set.iterator_prototype)); assert_eq!( heap.object_strong_count(prototype), Ok(prototype_strong + 1) @@ -787,20 +814,20 @@ fn for_in_iterator_advances_snapshots_and_transfers_current_edges() { assert_eq!(heap.object_strong_count(source), Ok(2)); heap.release_object(source).unwrap(); - assert_eq!( - heap.next_for_in_candidate(iterator), - Ok(ForInCandidate::Property { - object: source, - name: JsString::from_static("a"), - }) + let candidate = heap.next_for_in_candidate(iterator); + assert!( + matches!( + candidate, + Ok(ForInCandidate::Property { object, ref name }) + if object == source && name == &JsString::from_static("a") + ), + "unexpected candidate: {candidate:?}" ); - assert_eq!( + assert!(matches!( heap.next_for_in_candidate(iterator), - Ok(ForInCandidate::BaseComplete { - object: source, - fast_array: false, - }) - ); + Ok(ForInCandidate::BaseComplete { object, fast_array }) + if object == source && !fast_array + )); heap.enter_for_in_prototype_chain(iterator, None).unwrap(); let prototype = leaf(&mut heap, shape); @@ -817,25 +844,23 @@ fn for_in_iterator_advances_snapshots_and_transfers_current_edges() { assert_eq!(cleanup.finalized_objects, 1); assert!(matches!(heap.object(source), Err(HeapError::Stale { .. }))); heap.release_object(prototype).unwrap(); - assert_eq!( + assert!(matches!( heap.next_for_in_candidate(iterator), - Ok(ForInCandidate::Property { - object: prototype, - name: JsString::from_static("b"), - }) - ); - assert_eq!( + Ok(ForInCandidate::Property { object, ref name }) + if object == prototype && name == &JsString::from_static("b") + )); + assert!(matches!( heap.next_for_in_candidate(iterator), - Ok(ForInCandidate::LevelComplete(prototype)) - ); + Ok(ForInCandidate::LevelComplete(object)) if object == prototype + )); let cleanup = heap .replace_for_in_level(iterator, None, Vec::new()) .unwrap(); assert_eq!(cleanup.finalized_objects, 1); - assert_eq!( + assert!(matches!( heap.next_for_in_candidate(iterator), Ok(ForInCandidate::Done) - ); + )); heap.release_object(iterator).unwrap(); heap.release_shape(shape).unwrap(); diff --git a/src/engine/heap/tests/eval_bytecode.rs b/src/engine/heap/tests/eval_bytecode.rs index 88717837..0ef962d7 100644 --- a/src/engine/heap/tests/eval_bytecode.rs +++ b/src/engine/heap/tests/eval_bytecode.rs @@ -251,7 +251,7 @@ fn eval_variable_object_local_requires_exact_metadata_authentication() { .unwrap(); assert_eq!( heap.release_function_bytecode(published).unwrap().atoms, - vec![name] + vec![AtomIdx::from_raw(name.raw())] ); heap.release_context(context).unwrap(); heap.release_shape(shape).unwrap(); @@ -408,7 +408,7 @@ fn with_object_metadata_is_fail_closed_at_the_heap_boundary() { let published = heap.allocate_function_bytecode(local).unwrap(); assert_eq!( heap.release_function_bytecode(published).unwrap().atoms, - vec![name] + vec![AtomIdx::from_raw(name.raw())] ); let mut lexical = bytecode(&code, context, Vec::new(), vec![name]); @@ -617,7 +617,7 @@ fn bytecode_allocation_requires_published_closure_name_atom_ownership() { let published = heap.allocate_function_bytecode(published).unwrap(); assert_eq!( heap.release_function_bytecode(published).unwrap().atoms, - vec![name] + vec![AtomIdx::from_raw(name.raw())] ); heap.release_context(context).unwrap(); heap.release_shape(shape).unwrap(); @@ -664,7 +664,7 @@ fn eval_environment_closure_is_confined_to_direct_eval_root() { .unwrap(); assert_eq!( heap.release_function_bytecode(direct).unwrap().atoms, - vec![name] + vec![AtomIdx::from_raw(name.raw())] ); heap.release_context(context).unwrap(); heap.release_shape(shape).unwrap(); @@ -740,7 +740,7 @@ fn bytecode_allocation_requires_one_atom_reference_per_eval_binding_name() { .unwrap(); assert_eq!( heap.release_function_bytecode(published).unwrap().atoms, - vec![name, name] + vec![AtomIdx::from_raw(name.raw()), AtomIdx::from_raw(name.raw())] ); heap.release_context(context).unwrap(); heap.release_shape(shape).unwrap(); diff --git a/src/engine/heap/tests/function_lifetimes.rs b/src/engine/heap/tests/function_lifetimes.rs index 59b1981a..b1eaa022 100644 --- a/src/engine/heap/tests/function_lifetimes.rs +++ b/src/engine/heap/tests/function_lifetimes.rs @@ -55,7 +55,10 @@ fn two_closures_share_one_mutable_var_ref_cell() { heap.replace_var_ref_value(cell, RawValue::Int(9)).unwrap(), HeapCleanup::default() ); - assert_eq!(heap.var_ref(cell).unwrap().value, RawValue::Int(9)); + assert!(matches!( + heap.var_ref(cell).unwrap().value, + RawValue::Int(9) + )); assert_eq!(heap.release_var_ref(cell).unwrap(), HeapCleanup::default()); assert_eq!(heap.release_object(first).unwrap().finalized_objects, 1); @@ -435,6 +438,7 @@ fn bytecode_constant_pool_owns_child_and_returns_all_atoms() { let child_atom = Atom::from_raw(41); let parent_atom = Atom::from_raw(42); let symbol_atom = Atom::from_raw(43); + let symbol_index = AtomIdx::from_raw(symbol_atom.raw()); let child = heap .allocate_function_bytecode(bytecode(&code, context, Vec::new(), vec![child_atom])) .unwrap(); @@ -444,7 +448,7 @@ fn bytecode_constant_pool_owns_child_and_returns_all_atoms() { context, vec![ BytecodeConstant::Function(child), - BytecodeConstant::Value(RawValue::Symbol(symbol_atom)), + BytecodeConstant::Value(RawValue::Symbol(symbol_index)), ], vec![parent_atom], )) @@ -458,7 +462,11 @@ fn bytecode_constant_pool_owns_child_and_returns_all_atoms() { let mut cleanup = heap.release_function_bytecode(parent).unwrap(); assert_eq!(cleanup.finalized_function_bytecodes, 2); cleanup.atoms.sort_unstable(); - let mut expected = vec![child_atom, parent_atom, symbol_atom]; + let mut expected = vec![ + AtomIdx::from_raw(child_atom.raw()), + AtomIdx::from_raw(parent_atom.raw()), + symbol_index, + ]; expected.sort_unstable(); assert_eq!(cleanup.atoms, expected); @@ -542,14 +550,17 @@ fn async_function_intrinsic_root_is_a_function_prototype_child() { "AsyncFunction prototype does not inherit from Function.prototype" )) ); - assert_eq!(heap.context(realm).unwrap().async_function, None); + assert!(matches!(heap.context(realm).unwrap().async_function, None)); let roots = AsyncFunctionRealmData { function_prototype: async_function_prototype, }; let before = heap.object_strong_count(async_function_prototype).unwrap(); heap.attach_async_function_intrinsics(realm, roots).unwrap(); - assert_eq!(heap.context(realm).unwrap().async_function, Some(roots)); + assert!( + matches!(heap.context(realm).unwrap().async_function, Some(attached) + if attached.function_prototype == roots.function_prototype) + ); assert_eq!( heap.object_strong_count(async_function_prototype), Ok(before + 1) @@ -669,12 +680,12 @@ fn async_function_state_traces_callbacks_and_transfers_await_activation() { heap.async_function_state_snapshot(state).unwrap().phase, AsyncFunctionPhase::Executing ); - assert_eq!( + assert!(matches!( heap.begin_async_function_resume(state), Err(HeapError::Invariant( "AsyncFunction resume began outside an awaiting phase" )) - ); + )); let callback = heap .allocate_object(ObjectData::bound_internal_native_function( @@ -749,10 +760,11 @@ fn async_function_state_traces_callbacks_and_transfers_await_activation() { )) .unwrap(); let awaited_atom = Atom::from_raw(8_071); + let awaited_index = AtomIdx::from_raw(awaited_atom.raw()); let activation = GeneratorActivationData { bytecode, vm: GeneratorVmActivation { - stack: vec![RawValue::Symbol(awaited_atom)], + stack: vec![RawValue::Symbol(awaited_index)], regions: Vec::new(), pc: 2, callee_realm, @@ -791,7 +803,7 @@ fn async_function_state_traces_callbacks_and_transfers_await_activation() { ); assert_eq!( object_atoms(heap.object(state).unwrap()).collect::>(), - [awaited_atom] + [awaited_index] ); assert_eq!( heap.suspend_async_function(state, activation.clone()), @@ -800,8 +812,26 @@ fn async_function_state_traces_callbacks_and_transfers_await_activation() { )) ); let (resumed, cleanup) = heap.begin_async_function_resume(state).unwrap(); - assert_eq!(resumed, activation); - assert_eq!(cleanup.atoms, [awaited_atom]); + assert_eq!(resumed.bytecode, activation.bytecode); + assert_eq!(resumed.vm.stack.len(), 1); + assert!( + matches!(resumed.vm.stack[0], RawValue::Symbol(index) if index == awaited_index), + "resumed stack mismatch: {:?}", + resumed.vm.stack[0] + ); + assert_eq!(resumed.vm.pc, activation.vm.pc); + assert_eq!(resumed.vm.callee_realm, activation.vm.callee_realm); + assert_eq!(resumed.vm.current_function, activation.vm.current_function); + assert!(matches!(resumed.vm.this_value, RawValue::Undefined)); + assert!(resumed.vm.normalized_this.is_none() == activation.vm.normalized_this.is_none()); + assert!(matches!(resumed.vm.new_target, RawValue::Undefined)); + assert_eq!(resumed.vm.strict, activation.vm.strict); + assert_eq!(resumed.vm.callee_global, activation.vm.callee_global); + assert_eq!( + resumed.actual_argument_count, + activation.actual_argument_count + ); + assert_eq!(cleanup.atoms, [awaited_index]); assert_eq!( heap.async_function_state_snapshot(state).unwrap().phase, AsyncFunctionPhase::Executing @@ -830,7 +860,7 @@ fn async_function_state_traces_callbacks_and_transfers_await_activation() { .unwrap(); assert_eq!( heap.complete_async_function(awaiting_state).unwrap().atoms, - [awaited_atom] + [awaited_index] ); assert_eq!( heap.async_function_state_snapshot(awaiting_state) diff --git a/src/engine/heap/tests/modules.rs b/src/engine/heap/tests/modules.rs index 92985ea7..52092e1b 100644 --- a/src/engine/heap/tests/modules.rs +++ b/src/engine/heap/tests/modules.rs @@ -264,10 +264,10 @@ fn parsing_abort_tombstones_or_retains_a_hidden_stable_identity() { ); assert_eq!(heap.loaded_module_is_live(unreferenced), Ok(false)); assert!(heap.loaded_module(unreferenced).is_err()); - assert_eq!( + assert!(matches!( heap.first_loaded_module(realm, &JsString::from_static("unreferenced.js")), Ok(None) - ); + )); let direct_abort = heap .publish_loaded_module( @@ -288,19 +288,20 @@ fn parsing_abort_tombstones_or_retains_a_hidden_stable_identity() { )) ); assert_eq!(heap.loaded_module_is_live(direct_abort), Ok(true)); - assert_eq!( + assert!(matches!( heap.first_loaded_module(realm, &JsString::from_static("direct-abort.js")), - Ok(Some(direct_abort)) - ); + Ok(Some(found)) if found.cache == direct_abort.cache + && found.module == direct_abort.module + )); assert_eq!( heap.abort_parsing_loaded_module(direct_abort), Ok(HeapCleanup::default()) ); assert_eq!(heap.loaded_module_is_live(direct_abort), Ok(false)); - assert_eq!( + assert!(matches!( heap.first_loaded_module(realm, &JsString::from_static("direct-abort.js")), Ok(None) - ); + )); let first = heap .publish_loaded_module( @@ -335,10 +336,10 @@ fn parsing_abort_tombstones_or_retains_a_hidden_stable_identity() { heap.loaded_module(first).unwrap().body, RawModuleRecordBody::Aborted )); - assert_eq!( + assert!(matches!( heap.first_loaded_module(realm, &JsString::from_static("same.js")), - Ok(Some(second)) - ); + Ok(Some(found)) if found.cache == second.cache && found.module == second.module + )); assert_eq!( heap.loaded_modules(realm) .unwrap() diff --git a/src/engine/heap/tests/objects.rs b/src/engine/heap/tests/objects.rs index fb019f5e..45faeb66 100644 --- a/src/engine/heap/tests/objects.rs +++ b/src/engine/heap/tests/objects.rs @@ -51,15 +51,11 @@ fn proxy_revocation_releases_only_the_one_shot_closure_capture() { .unwrap(); assert_eq!(heap.object_strong_count(target), Ok(2)); assert_eq!(heap.object_strong_count(handler), Ok(2)); - assert_eq!( - heap.proxy_snapshot(proxy), - Ok(ProxyData { - target, - handler, - is_callable: false, - is_revoked: false, - }) - ); + let snapshot = heap.proxy_snapshot(proxy).unwrap(); + assert_eq!(snapshot.target, target); + assert_eq!(snapshot.handler, handler); + assert!(!snapshot.is_callable); + assert!(!snapshot.is_revoked); let revoker = heap .allocate_object(ObjectData::bound_internal_native_function( @@ -80,10 +76,10 @@ fn proxy_revocation_releases_only_the_one_shot_closure_capture() { assert!(heap.proxy_snapshot(proxy).unwrap().is_revoked); assert_eq!(heap.object_strong_count(target), Ok(2)); assert_eq!(heap.object_strong_count(handler), Ok(2)); - assert_eq!( + assert!(matches!( heap.native_internal_callable(revoker), Ok(Some(InternalCallableData::ProxyRevoke { proxy: None })) - ); + )); let (revoked_again, cleanup) = heap.revoke_proxy_from_callable(revoker).unwrap(); assert!(!revoked_again); @@ -287,6 +283,7 @@ fn primitive_object_payload_category_is_structurally_validated() { assert_eq!(object_edges(string_data), vec![RawId::Shape(shape)]); assert_eq!(object_atoms(string_data).count(), 0); + let symbol_index = AtomIdx::from_raw(symbol_atom.raw()); let symbol = heap .allocate_object(ObjectData::primitive( shape, @@ -300,7 +297,10 @@ fn primitive_object_payload_category_is_structurally_validated() { ObjectPayload::Primitive(PrimitiveObjectData::Symbol(atom)) if atom == symbol_atom )); assert_eq!(object_edges(symbol_data), vec![RawId::Shape(shape)]); - assert_eq!(object_atoms(symbol_data).collect::>(), [symbol_atom]); + assert_eq!( + object_atoms(symbol_data).collect::>(), + [symbol_index] + ); let bigint = heap .allocate_object(ObjectData::primitive( @@ -320,7 +320,7 @@ fn primitive_object_payload_category_is_structurally_validated() { heap.release_object(bigint).unwrap(); let symbol_cleanup = heap.release_object(symbol).unwrap(); - assert_eq!(symbol_cleanup.atoms, [symbol_atom]); + assert_eq!(symbol_cleanup.atoms, [symbol_index]); let string_cleanup = heap.release_object(string).unwrap(); assert!(string_cleanup.atoms.is_empty()); heap.release_object(number).unwrap(); @@ -415,18 +415,18 @@ fn regexp_payload_is_branded_edge_free_and_structurally_validated() { let ordinary = heap .allocate_object(ObjectData::ordinary(shape, Vec::new())) .unwrap(); - assert_eq!( + assert!(matches!( heap.regexp_data(ordinary), Err(HeapError::Invariant( "RegExp data requested for an object with the wrong class" )) - ); - assert_eq!( + )); + assert!(matches!( heap.replace_regexp_data(ordinary, RegExpObjectData::Uninitialized), Err(HeapError::Invariant( "RegExp data update reached an object with the wrong class" )) - ); + )); heap.release_object(ordinary).unwrap(); heap.release_object(regexp).unwrap(); @@ -455,7 +455,10 @@ fn compiled_regexp_payload_replacement_and_finalization_release_rc_leaves() { }, ) .unwrap(); - assert_eq!(previous, RegExpObjectData::Uninitialized); + assert!( + matches!(previous, RegExpObjectData::Uninitialized), + "previous payload mismatch: {previous:?}" + ); assert_eq!(Rc::strong_count(&program), 2); let RegExpObjectData::Compiled { pattern: stored_pattern, diff --git a/src/engine/heap/tests/storage.rs b/src/engine/heap/tests/storage.rs index ba33ef2b..5b7709f1 100644 --- a/src/engine/heap/tests/storage.rs +++ b/src/engine/heap/tests/storage.rs @@ -117,19 +117,23 @@ fn iterator_payload_mutations_retain_replacements_and_completion_keeps_edges() { heap.set_iterator_helper_running(helper, true).unwrap(); heap.set_iterator_helper_done_and_running(helper, true, false) .unwrap(); - assert_eq!( - heap.iterator_helper_state(helper).unwrap(), - IteratorHelperData { - source: replacement_source, - next: RawValue::Object(replacement_next), - callback: RawValue::Object(replacement_callback), - inner: Some(replacement_inner), - count: 7, - kind: IteratorHelperKind::FlatMap, - executing: false, - done: true, - } + let state = heap.iterator_helper_state(helper).unwrap(); + assert_eq!(state.source, replacement_source); + assert!( + matches!(state.next, RawValue::Object(object) if object == replacement_next), + "next mismatch: {:?}", + state.next ); + assert!( + matches!(state.callback, RawValue::Object(object) if object == replacement_callback), + "callback mismatch: {:?}", + state.callback + ); + assert_eq!(state.inner, Some(replacement_inner)); + assert_eq!(state.count, 7); + assert_eq!(state.kind, IteratorHelperKind::FlatMap); + assert!(!state.executing); + assert!(state.done); for edge in [ replacement_source, replacement_next, @@ -190,13 +194,11 @@ fn iterator_wrap_source_and_cached_next_are_owned_edges() { .unwrap(); heap.set_iterator_wrap_next(wrapper, RawValue::Object(replacement_next)) .unwrap(); - assert_eq!( + assert!(matches!( heap.iterator_wrap_state(wrapper), - Ok(( - RawValue::Object(replacement_source), - RawValue::Object(replacement_next) - )) - ); + Ok((RawValue::Object(source), RawValue::Object(next))) + if source == replacement_source && next == replacement_next + )); assert_eq!(heap.object_strong_count(source), Ok(1)); assert_eq!(heap.object_strong_count(next), Ok(1)); assert_eq!(heap.object_strong_count(replacement_source), Ok(2)); @@ -206,7 +208,7 @@ fn iterator_wrap_source_and_cached_next_are_owned_edges() { assert_eq!(heap.object_strong_count(replacement_source), Ok(1)); assert_eq!(heap.object_strong_count(replacement_next), Ok(1)); - let symbol = Atom::from_immediate_integer(17).unwrap(); + let symbol = AtomIdx::from_raw(Atom::from_immediate_integer(17).unwrap().raw()); let symbol_wrapper = heap .allocate_object(ObjectData::iterator_wrap( shape, @@ -221,7 +223,7 @@ fn iterator_wrap_source_and_cached_next_are_owned_edges() { assert_eq!(cleanup.atoms, vec![symbol]); heap.release_object(symbol_wrapper).unwrap(); - let source_symbol = Atom::from_immediate_integer(19).unwrap(); + let source_symbol = AtomIdx::from_raw(Atom::from_immediate_integer(19).unwrap().raw()); let primitive_wrapper = heap .allocate_object(ObjectData::iterator_wrap( shape, @@ -230,10 +232,10 @@ fn iterator_wrap_source_and_cached_next_are_owned_edges() { RawValue::Undefined, )) .unwrap(); - assert_eq!( + assert!(matches!( heap.iterator_wrap_state(primitive_wrapper), - Ok((RawValue::Symbol(source_symbol), RawValue::Undefined)) - ); + Ok((RawValue::Symbol(index), RawValue::Undefined)) if index == source_symbol + )); let cleanup = heap.release_object(primitive_wrapper).unwrap(); assert_eq!(cleanup.atoms, vec![source_symbol]); @@ -261,15 +263,15 @@ fn async_from_sync_iterator_owns_source_cached_next_and_symbol_atom() { assert_eq!(heap.object_strong_count(source), Ok(2)); assert_eq!(heap.object_strong_count(next), Ok(2)); - assert_eq!( + assert!(matches!( heap.async_from_sync_iterator_state(wrapper), - Ok((source, RawValue::Object(next))) - ); + Ok((source, RawValue::Object(next_id))) if next_id == next + )); heap.release_object(wrapper).unwrap(); assert_eq!(heap.object_strong_count(source), Ok(1)); assert_eq!(heap.object_strong_count(next), Ok(1)); - let symbol = Atom::from_immediate_integer(29).unwrap(); + let symbol = AtomIdx::from_raw(Atom::from_immediate_integer(29).unwrap().raw()); let symbol_wrapper = heap .allocate_object(ObjectData::async_from_sync_iterator( shape, @@ -372,22 +374,20 @@ fn iterator_concat_releases_consumed_and_drained_edges_at_quickjs_boundaries() { for edge in [iterable_b, method_b] { assert_eq!(heap.object_strong_count(edge), Ok(2)); } - assert_eq!( - heap.iterator_concat_state(concat).unwrap(), - IteratorConcatData { - items: vec![ - None, - Some(IteratorConcatItem { - iterable: iterable_b, - method: RawValue::Object(method_b), - }), - ], - index: 1, - iterator: None, - next: RawValue::Undefined, - running: false, - } + let state = heap.iterator_concat_state(concat).unwrap(); + assert_eq!(state.items.len(), 2); + assert!(state.items[0].is_none()); + let second = state.items[1].as_ref().expect("second concat item"); + assert_eq!(second.iterable, iterable_b); + assert!( + matches!(second.method, RawValue::Object(object) if object == method_b), + "method mismatch: {:?}", + second.method ); + assert_eq!(state.index, 1); + assert_eq!(state.iterator, None); + assert!(matches!(state.next, RawValue::Undefined)); + assert!(!state.running); heap.set_iterator_concat_iterator(concat, Some(active_b)) .unwrap(); @@ -397,16 +397,13 @@ fn iterator_concat_releases_consumed_and_drained_edges_at_quickjs_boundaries() { for edge in [iterable_b, method_b, active_b, next_b] { assert_eq!(heap.object_strong_count(edge), Ok(1)); } - assert_eq!( - heap.iterator_concat_state(concat).unwrap(), - IteratorConcatData { - items: vec![None, None], - index: 2, - iterator: None, - next: RawValue::Undefined, - running: false, - } - ); + let state = heap.iterator_concat_state(concat).unwrap(); + assert_eq!(state.items.len(), 2); + assert!(state.items.iter().all(|item| item.is_none())); + assert_eq!(state.index, 2); + assert_eq!(state.iterator, None); + assert!(matches!(state.next, RawValue::Undefined)); + assert!(!state.running); heap.release_object(concat).unwrap(); for object in [ @@ -468,18 +465,19 @@ fn iterator_intrinsics_attach_transactionally_and_form_a_collectable_realm_cycle "Iterator Helper prototype is not an ordinary child of the realm's Iterator prototype", )) ); - assert_eq!(heap.context(realm).unwrap().iterator, None); + assert!(matches!(heap.context(realm).unwrap().iterator, None)); heap.live_node_mut(RawId::Object(wrap_prototype)) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); assert_eq!( heap.attach_iterator_intrinsics(realm, iterator), Err(HeapError::Overflow { operation: "retaining outgoing heap edges", }) ); - assert_eq!(heap.context(realm).unwrap().iterator, None); + assert!(matches!(heap.context(realm).unwrap().iterator, None)); assert_eq!( heap.object_strong_count(constructor), Ok(constructor_strong) @@ -494,10 +492,17 @@ fn iterator_intrinsics_attach_transactionally_and_form_a_collectable_realm_cycle ); heap.live_node_mut(RawId::Object(wrap_prototype)) .unwrap() - .strong = wrap_strong; + .strong + .set(wrap_strong); heap.attach_iterator_intrinsics(realm, iterator).unwrap(); - assert_eq!(heap.context(realm).unwrap().iterator, Some(iterator)); + assert!( + matches!(heap.context(realm).unwrap().iterator, Some(attached) + if attached.constructor == iterator.constructor + && attached.concat_prototype == iterator.concat_prototype + && attached.helper_prototype == iterator.helper_prototype + && attached.wrap_prototype == iterator.wrap_prototype) + ); assert_eq!( heap.object_strong_count(constructor), Ok(constructor_strong + 1) @@ -649,7 +654,7 @@ fn regexp_fixture() -> RegExpFixture { Shape::new( Some(prototype), [ShapeEntry { - atom: last_index, + atom: AtomIdx::from_raw(last_index.raw()), flags: PropertyFlags::data(true, false, false), }], ) @@ -693,7 +698,8 @@ fn regexp_intrinsics_attach_transactionally_once_and_finalize_with_realm() { .heap .live_node_mut(RawId::Shape(fixture.object_shape)) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); assert_eq!( fixture .heap @@ -702,7 +708,10 @@ fn regexp_intrinsics_attach_transactionally_once_and_finalize_with_realm() { operation: "retaining outgoing heap edges", }) ); - assert_eq!(fixture.heap.context(fixture.realm).unwrap().regexp, None); + assert!(matches!( + fixture.heap.context(fixture.realm).unwrap().regexp, + None + )); assert_eq!( fixture.heap.object_strong_count(fixture.prototype).unwrap(), prototype_strong @@ -725,15 +734,20 @@ fn regexp_intrinsics_attach_transactionally_once_and_finalize_with_realm() { .heap .live_node_mut(RawId::Shape(fixture.object_shape)) .unwrap() - .strong = object_shape_strong; + .strong + .set(object_shape_strong); fixture .heap .attach_regexp_intrinsics(fixture.realm, realm_data, fixture.last_index) .unwrap(); - assert_eq!( - fixture.heap.context(fixture.realm).unwrap().regexp, - Some(realm_data) + assert!( + matches!(fixture.heap.context(fixture.realm).unwrap().regexp, Some(attached) + if attached.prototype == realm_data.prototype + && attached.constructor == realm_data.constructor + && attached.string_iterator_prototype == realm_data.string_iterator_prototype + && attached.object_shape == realm_data.object_shape + && attached.result_shapes == realm_data.result_shapes) ); assert_eq!( fixture.heap.object_strong_count(fixture.prototype).unwrap(), @@ -901,7 +915,7 @@ fn regexp_intrinsics_reject_mismatched_constructor_prototype_and_shape() { Shape::new( Some(fixture.root), [ShapeEntry { - atom: fixture.last_index, + atom: AtomIdx::from_raw(fixture.last_index.raw()), flags: PropertyFlags::data(true, false, false), }], ) @@ -929,7 +943,7 @@ fn regexp_intrinsics_reject_mismatched_constructor_prototype_and_shape() { Shape::new( Some(fixture.prototype), [ShapeEntry { - atom: fixture.last_index, + atom: AtomIdx::from_raw(fixture.last_index.raw()), flags: DATA_FLAGS, }], ) @@ -958,7 +972,10 @@ fn regexp_intrinsics_reject_mismatched_constructor_prototype_and_shape() { .attach_regexp_intrinsics(fixture.realm, realm_data, wrong_atom) .is_err() ); - assert_eq!(fixture.heap.context(fixture.realm).unwrap().regexp, None); + assert!(matches!( + fixture.heap.context(fixture.realm).unwrap().regexp, + None + )); fixture.dispose(); } @@ -1153,7 +1170,7 @@ fn object_slot_replacement_rejects_private_name_payloads() { vec![PropertySlot::Data(RawValue::Undefined)], )) .unwrap(); - let private = Atom::from_raw(91); + let private = AtomIdx::from_raw(91); assert_eq!( heap.replace_object_slot(object, 0, PropertySlot::Data(RawValue::Private(private)),), @@ -1267,13 +1284,13 @@ fn forged_cross_kind_handle_reports_wrong_kind() { generation: shape.generation, }; - assert_eq!( + assert!(matches!( heap.object(forged), Err(HeapError::WrongKind { expected: HeapNodeKind::Object, actual: HeapNodeKind::Shape, }) - ); + )); heap.release_shape(shape).unwrap(); } @@ -1322,7 +1339,10 @@ fn property_slot_transaction_retains_before_publish_and_marks_cleanup_failures() vec![PropertySlot::Data(RawValue::Undefined)], )) .unwrap(); - heap.live_node_mut(RawId::Object(target)).unwrap().strong = u32::MAX; + heap.live_node_mut(RawId::Object(target)) + .unwrap() + .strong + .set(u32::MAX); let failure = heap .replace_object_slot_with_status(object, 0, PropertySlot::Data(RawValue::Object(target))) .err() @@ -1333,10 +1353,16 @@ fn property_slot_transaction_retains_before_publish_and_marks_cleanup_failures() 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.live_node_mut(RawId::Object(target)) + .unwrap() + .strong + .set(1); heap.replace_object_slot(object, 0, PropertySlot::Data(RawValue::Object(target))) .unwrap(); - heap.live_node_mut(RawId::Object(target)).unwrap().strong = 0; + heap.live_node_mut(RawId::Object(target)) + .unwrap() + .strong + .set(0); let failure = heap .replace_object_slot_with_status(object, 0, PropertySlot::Data(RawValue::Int(42))) .err() @@ -1346,7 +1372,10 @@ fn property_slot_transaction_retains_before_publish_and_marks_cleanup_failures() heap.object(object).unwrap().slots[0], PropertySlot::Data(RawValue::Int(42)) )); - heap.live_node_mut(RawId::Object(target)).unwrap().strong = 1; + heap.live_node_mut(RawId::Object(target)) + .unwrap() + .strong + .set(1); heap.release_object(target).unwrap(); heap.release_object(object).unwrap(); heap.release_shape(shape).unwrap(); @@ -1376,29 +1405,32 @@ fn property_slot_transaction_keeps_new_symbol_owned_after_post_publish_failure() { let mut state = runtime.0.state.borrow_mut(); let symbol = state.atoms.new_symbol(Some("replacement")).unwrap(); + let symbol_index = AtomIdx::from_raw(symbol.raw()); state .heap .live_node_mut(RawId::Object(old.object_id())) .unwrap() - .strong = 0; + .strong + .set(0); assert!( state .replace_property_slot( object.object_id(), 0, - PropertySlot::Data(RawValue::Symbol(symbol)) + PropertySlot::Data(RawValue::Symbol(symbol_index)) ) .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) + matches!(state.heap.object(object.object_id()).unwrap().slots[0], PropertySlot::Data(RawValue::Symbol(index)) if index == symbol_index) ); state .heap .live_node_mut(RawId::Object(old.object_id())) .unwrap() - .strong = 1; + .strong + .set(1); state.atoms.release(symbol).unwrap(); } drop(object); diff --git a/src/engine/heap/tests/weak_collections.rs b/src/engine/heap/tests/weak_collections.rs index 119ee678..b16279d9 100644 --- a/src/engine/heap/tests/weak_collections.rs +++ b/src/engine/heap/tests/weak_collections.rs @@ -67,7 +67,11 @@ fn finalization_registry_transfers_held_and_job_roots_without_retain_on_adoption let job = sink.jobs.front().unwrap(); assert_eq!(job.callback, callback); assert_eq!(job.realm, realm); - assert_eq!(job.held_value, RawValue::Object(held)); + assert!( + matches!(job.held_value, RawValue::Object(object) if object == held), + "held value mismatch: {:?}", + job.held_value + ); // Releasing the registry removes only its own callback/realm roots; // the already-published job keeps its roots without another retain. @@ -85,6 +89,7 @@ fn finalization_registry_transfers_held_and_job_roots_without_retain_on_adoption fn finalization_job_moves_held_symbol_ownership_until_job_release() { let mut atoms = crate::engine::atom::AtomTable::new(); let held = atoms.new_symbol(Some("finalization held value")).unwrap(); + let held_index = AtomIdx::from_raw(held.raw()); let mut heap = Heap::new(); let shape = empty_shape(&mut heap); let (root, function_prototype, realm, callback) = finalization_test_realm(&mut heap, shape); @@ -95,7 +100,7 @@ fn finalization_job_moves_held_symbol_ownership_until_job_release() { heap.finalization_registry_register( registry, WeakCollectionKey::Object(target), - RawValue::Symbol(held), + RawValue::Symbol(held_index), None, ) .unwrap(); @@ -105,10 +110,10 @@ fn finalization_job_moves_held_symbol_ownership_until_job_release() { heap.run_gc_with_finalization_sink( |event| { Ok(match event { - WeakSymbolGcEvent::IsLive(atom) => atoms.is_live(atom), - WeakSymbolGcEvent::Release(atom) => { + WeakSymbolGcEvent::IsLive(index) => atoms.is_live_index(index), + WeakSymbolGcEvent::Release(index) => { atoms - .release(atom) + .release_index(index) .map_err(|_| HeapError::Invariant("held Symbol release failed"))?; true } @@ -118,12 +123,16 @@ fn finalization_job_moves_held_symbol_ownership_until_job_release() { ) .unwrap(); assert_eq!(sink.jobs.len(), 1); - assert_eq!(sink.jobs[0].held_value, RawValue::Symbol(held)); + assert!( + matches!(sink.jobs[0].held_value, RawValue::Symbol(index) if index == held_index), + "held value mismatch: {:?}", + sink.jobs[0].held_value + ); assert!(atoms.is_live(held)); heap.release_object(registry).unwrap(); let cleanup = heap.discard_finalization_jobs(sink.jobs).unwrap(); - assert_eq!(cleanup.atoms, [held]); + assert_eq!(cleanup.atoms, [held_index]); assert_eq!( atoms.release(held), Ok(crate::engine::atom::ReleaseOutcome::Removed) @@ -351,8 +360,16 @@ fn finalization_registry_unregister_is_allocation_free_and_stable() { unreachable!() }; assert_eq!(data.entries.len(), 2); - assert_eq!(data.entries[0].held_value, RawValue::Object(held[1])); - assert_eq!(data.entries[1].held_value, RawValue::Object(held[3])); + assert!( + matches!(data.entries[0].held_value, RawValue::Object(object) if object == held[1]), + "held value mismatch: {:?}", + data.entries[0].held_value + ); + assert!( + matches!(data.entries[1].held_value, RawValue::Object(object) if object == held[3]), + "held value mismatch: {:?}", + data.entries[1].held_value + ); assert!(matches!(heap.object(held[0]), Err(HeapError::Stale { .. }))); assert!(matches!(heap.object(held[2]), Err(HeapError::Stale { .. }))); @@ -533,6 +550,7 @@ fn weak_map_prunes_records_incrementally_in_insertion_order() { fn weak_symbol_hook_release_precedes_later_record_liveness_query() { let mut atoms = crate::engine::atom::AtomTable::new(); let symbol = atoms.new_symbol(Some("ordered weak key")).unwrap(); + let symbol_index = AtomIdx::from_raw(symbol.raw()); let mut heap = Heap::new(); let shape = empty_shape(&mut heap); let weak_map = heap @@ -544,7 +562,7 @@ fn weak_symbol_hook_release_precedes_later_record_liveness_query() { let weak_symbol = WeakCollectionKey::Symbol(symbol); // The first value owns the symbol used non-owningly by the next key. - heap.weak_map_set(weak_map, weak_first, RawValue::Symbol(symbol)) + heap.weak_map_set(weak_map, weak_first, RawValue::Symbol(symbol_index)) .unwrap(); heap.weak_map_set(weak_map, weak_symbol, RawValue::Object(held_value)) .unwrap(); @@ -557,9 +575,9 @@ fn weak_symbol_hook_release_precedes_later_record_liveness_query() { |event| { events.push(event); match event { - WeakSymbolGcEvent::IsLive(atom) => Ok(atoms.is_live(atom)), - WeakSymbolGcEvent::Release(atom) => { - atoms.release(atom).map_err(|_| { + WeakSymbolGcEvent::IsLive(index) => Ok(atoms.is_live_index(index)), + WeakSymbolGcEvent::Release(index) => { + atoms.release_index(index).map_err(|_| { HeapError::Invariant("weak-symbol test hook release failed") })?; Ok(true) @@ -572,12 +590,12 @@ fn weak_symbol_hook_release_precedes_later_record_liveness_query() { assert_eq!( events, vec![ - WeakSymbolGcEvent::Release(symbol), - WeakSymbolGcEvent::IsLive(symbol) + WeakSymbolGcEvent::Release(symbol_index), + WeakSymbolGcEvent::IsLive(symbol_index) ] ); assert!(!atoms.is_live(symbol)); - assert!(!stats.cleanup.atoms.contains(&symbol)); + assert!(!stats.cleanup.atoms.contains(&symbol_index)); assert_eq!(stats.cleanup.finalized_objects, 1); assert!(heap.weak_map_get(weak_map, weak_first).unwrap().is_none()); assert!(heap.weak_map_get(weak_map, weak_symbol).unwrap().is_none()); @@ -591,6 +609,7 @@ fn weak_symbol_hook_release_precedes_later_record_liveness_query() { fn gc_release_hook_can_defer_detached_value_atoms() { let mut atoms = crate::engine::atom::AtomTable::new(); let symbol = atoms.new_symbol(Some("deferred weak value")).unwrap(); + let symbol_index = AtomIdx::from_raw(symbol.raw()); let mut heap = Heap::new(); let shape = empty_shape(&mut heap); let weak_map = heap @@ -600,7 +619,7 @@ fn gc_release_hook_can_defer_detached_value_atoms() { heap.weak_map_set( weak_map, WeakCollectionKey::Object(key), - RawValue::Symbol(symbol), + RawValue::Symbol(symbol_index), ) .unwrap(); heap.release_object(key).unwrap(); @@ -609,7 +628,7 @@ fn gc_release_hook_can_defer_detached_value_atoms() { .run_gc_with_finalization_sink( |event| { Ok(match event { - WeakSymbolGcEvent::IsLive(atom) => atoms.is_live(atom), + WeakSymbolGcEvent::IsLive(index) => atoms.is_live_index(index), WeakSymbolGcEvent::Release(_) => false, }) }, @@ -617,7 +636,7 @@ fn gc_release_hook_can_defer_detached_value_atoms() { ) .unwrap(); assert!(atoms.is_live(symbol)); - assert_eq!(stats.cleanup.atoms, vec![symbol]); + assert_eq!(stats.cleanup.atoms, vec![symbol_index]); assert!(atoms.release(symbol).is_ok()); heap.release_object(weak_map).unwrap(); @@ -816,6 +835,7 @@ fn cycle_collected_weak_key_is_pruned_on_the_following_gc() { fn stale_symbol_keys_are_pruned_without_owning_the_atom() { let mut atoms = crate::engine::atom::AtomTable::new(); let symbol = atoms.new_symbol(Some("weak key")).unwrap(); + let symbol_index = AtomIdx::from_raw(symbol.raw()); let mut heap = Heap::new(); let shape = empty_shape(&mut heap); let weak_map = heap @@ -841,7 +861,7 @@ fn stale_symbol_keys_are_pruned_without_owning_the_atom() { .run_gc_with_finalization_sink( |event| { Ok(match event { - WeakSymbolGcEvent::IsLive(atom) => atoms.is_live(atom), + WeakSymbolGcEvent::IsLive(index) => atoms.is_live_index(index), WeakSymbolGcEvent::Release(_) => false, }) }, @@ -849,7 +869,7 @@ fn stale_symbol_keys_are_pruned_without_owning_the_atom() { ) .unwrap(); assert_eq!(stats.cleanup.finalized_objects, 1); - assert!(!stats.cleanup.atoms.contains(&symbol)); + assert!(!stats.cleanup.atoms.contains(&symbol_index)); assert!(heap.weak_map_get(weak_map, weak_key).unwrap().is_none()); assert!(!heap.weak_set_has(weak_set, weak_key).unwrap()); @@ -881,9 +901,12 @@ fn weak_map_hash_storage_handles_the_deep_staging_scale() { keys.push(key); } for (index, key) in keys.iter().copied().enumerate() { - assert_eq!( - heap.weak_map_get(weak_map, WeakCollectionKey::Object(key)), - Ok(Some(&RawValue::Int(i32::try_from(index).unwrap()))) + assert!( + matches!( + heap.weak_map_get(weak_map, WeakCollectionKey::Object(key)), + Ok(Some(&RawValue::Int(value))) if value == i32::try_from(index).unwrap() + ), + "weak map value mismatch for key {index}" ); } @@ -960,7 +983,7 @@ fn weak_reference_intrinsics_attach_atomically_and_root_both_prototypes() { root, root, root, root, root, root, root, root, )) .unwrap(); - assert_eq!(heap.context(realm).unwrap().weak_ref, None); + assert!(matches!(heap.context(realm).unwrap().weak_ref, None)); let intrinsic_shape = heap .allocate_shape(Shape::new(Some(root), []).unwrap()) @@ -992,7 +1015,7 @@ fn weak_reference_intrinsics_attach_atomically_and_root_both_prototypes() { "WeakRef and FinalizationRegistry prototypes share one identity", )) ); - assert_eq!(heap.context(realm).unwrap().weak_ref, None); + assert!(matches!(heap.context(realm).unwrap().weak_ref, None)); assert_eq!( heap.attach_weak_ref_intrinsics( @@ -1006,7 +1029,7 @@ fn weak_reference_intrinsics_attach_atomically_and_root_both_prototypes() { "WeakRef prototype is not an ordinary child of Object.prototype", )) ); - assert_eq!(heap.context(realm).unwrap().weak_ref, None); + assert!(matches!(heap.context(realm).unwrap().weak_ref, None)); assert_eq!( heap.object_strong_count(weak_ref_prototype), Ok(weak_ref_strong) @@ -1018,24 +1041,31 @@ fn weak_reference_intrinsics_attach_atomically_and_root_both_prototypes() { heap.live_node_mut(RawId::Object(finalization_registry_prototype)) .unwrap() - .strong = u32::MAX; + .strong + .set(u32::MAX); assert_eq!( heap.attach_weak_ref_intrinsics(realm, roots), Err(HeapError::Overflow { operation: "retaining outgoing heap edges", }) ); - assert_eq!(heap.context(realm).unwrap().weak_ref, None); + assert!(matches!(heap.context(realm).unwrap().weak_ref, None)); assert_eq!( heap.object_strong_count(weak_ref_prototype), Ok(weak_ref_strong) ); heap.live_node_mut(RawId::Object(finalization_registry_prototype)) .unwrap() - .strong = finalization_registry_strong; + .strong + .set(finalization_registry_strong); heap.attach_weak_ref_intrinsics(realm, roots).unwrap(); - assert_eq!(heap.context(realm).unwrap().weak_ref, Some(roots)); + assert!( + matches!(heap.context(realm).unwrap().weak_ref, Some(attached) + if attached.weak_ref_prototype == roots.weak_ref_prototype + && attached.finalization_registry_prototype + == roots.finalization_registry_prototype) + ); assert_eq!( heap.object_strong_count(weak_ref_prototype), Ok(weak_ref_strong + 1) diff --git a/src/engine/heap/value_storage.rs b/src/engine/heap/value_storage.rs new file mode 100644 index 00000000..2b180427 --- /dev/null +++ b/src/engine/heap/value_storage.rs @@ -0,0 +1,42 @@ +use super::*; + +impl Heap { + /// Read one live string node payload. + pub fn string(&self, id: StringId) -> Result<&JsString, HeapError> { + match self.live_node(RawId::String(id))?.data { + NodeData::String(ref value) => Ok(value), + NodeData::Object(_) + | NodeData::Shape(_) + | NodeData::VarRef(_) + | NodeData::Context(_) + | NodeData::FunctionBytecode(_) + | NodeData::BigInt(_) => Err(HeapError::Invariant( + "typed string lookup reached another node payload", + )), + } + } + + /// Trusted shared read for a live `StringId` held by an owning edge. + #[inline] + pub(crate) fn string_fast(&self, id: StringId) -> &JsString { + match &self.live_node_fast(RawId::String(id)).data { + NodeData::String(value) => value, + _ => unreachable!("trusted string handle reached another node payload"), + } + } + + /// Read one live BigInt node payload. + pub fn bigint(&self, id: BigIntId) -> Result<&JsBigInt, HeapError> { + match self.live_node(RawId::BigInt(id))?.data { + NodeData::BigInt(ref value) => Ok(value), + NodeData::Object(_) + | NodeData::Shape(_) + | NodeData::VarRef(_) + | NodeData::Context(_) + | NodeData::FunctionBytecode(_) + | NodeData::String(_) => Err(HeapError::Invariant( + "typed bigint lookup reached another node payload", + )), + } + } +} diff --git a/src/engine/jobs/mod.rs b/src/engine/jobs/mod.rs index 02f595fe..6379286c 100644 --- a/src/engine/jobs/mod.rs +++ b/src/engine/jobs/mod.rs @@ -323,8 +323,16 @@ impl RuntimeState { let cleanup = self.heap.release_object(*object)?; self.apply_cleanup(cleanup)?; } - RawValue::Symbol(atom) => { - self.atoms.release(*atom)?; + RawValue::Symbol(index) => { + self.atoms.release_index(*index)?; + } + RawValue::String(id) => { + let cleanup = self.heap.release_string(*id)?; + self.apply_cleanup(cleanup)?; + } + RawValue::BigInt(id) => { + let cleanup = self.heap.release_bigint(*id)?; + self.apply_cleanup(cleanup)?; } RawValue::Private(_) => { return Err(RuntimeError::Invariant( @@ -335,9 +343,7 @@ impl RuntimeState { | RawValue::Null | RawValue::Bool(_) | RawValue::Int(_) - | RawValue::Float(_) - | RawValue::BigInt(_) - | RawValue::String(_) => {} + | RawValue::Float(_) => {} RawValue::Uninitialized | RawValue::Exception => { return Err(RuntimeError::Invariant( "internal value sentinel occupied a pending job root", @@ -467,11 +473,11 @@ impl Runtime { .execute_pending_job_record(job.record()) .and_then(|completion| match completion { Completion::Return(value) => { - drop(value); + self.release_jsvalue(value)?; Ok(false) } Completion::Throw(value) => { - self.set_pending_exception(value)?; + self.set_pending_exception_jsvalue(value)?; Ok(true) } }); diff --git a/src/engine/modules/body.rs b/src/engine/modules/body.rs index 0ea1a9ab..b852626b 100644 --- a/src/engine/modules/body.rs +++ b/src/engine/modules/body.rs @@ -9,7 +9,7 @@ use crate::engine::heap::{ ContextId, InternalCallableData, PromiseState, RawModuleRef, roots::VarRefRoot, }; use crate::engine::object::CallableRef; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; pub(crate) enum BodyStep { @@ -72,8 +72,8 @@ impl BodyStep { ))?; let slot = VarRefRoot::from_borrowed_handle(runtime.clone(), slot)?; let default_value = runtime.root_raw_value(default_value)?; - runtime.write_var_ref(&slot, default_value)?; - Ok(Self::Complete(Completion::Return(Value::Undefined))) + runtime.write_var_ref(&slot, runtime.into_jsvalue(default_value)?)?; + Ok(Self::Complete(Completion::Return(JsValue::Undefined))) } ModuleRecordBody::Parsing => Err(RuntimeError::Invariant( "module execution reached a parse-in-progress record", @@ -93,15 +93,26 @@ impl BodyResume { Phase::Attach => { if let Completion::Throw(reason) = completion { // Preserve the ignored abrupt attachment and pending exception. - runtime.set_pending_exception(reason)?; + runtime.set_pending_exception_jsvalue(reason)?; } - Ok(BodyStep::Complete(Completion::Return(Value::Undefined))) + Ok(BodyStep::Complete(Completion::Return(JsValue::Undefined))) } Phase::Async => { - let Completion::Return(Value::Object(promise)) = completion else { - return Err(RuntimeError::Invariant( - "async module callable did not return a Promise", - )); + let promise = match completion { + Completion::Return(value) => match runtime.root_and_release_jsvalue(value)? { + Value::Object(promise) => promise, + _ => { + return Err(RuntimeError::Invariant( + "async module callable did not return a Promise", + )); + } + }, + Completion::Throw(value) => { + runtime.release_jsvalue(value)?; + return Err(RuntimeError::Invariant( + "async module callable did not return a Promise", + )); + } }; let module = self.root.raw; let make_handler = |kind| { @@ -126,15 +137,21 @@ impl BodyResume { } } fn inspect_sync(runtime: &Runtime, completion: Completion) -> Result { - let Completion::Return(Value::Object(promise)) = completion else { - return match completion { - Completion::Throw(_) => Err(RuntimeError::Invariant( + let promise = match completion { + Completion::Return(value) => match runtime.root_and_release_jsvalue(value)? { + Value::Object(promise) => promise, + _ => { + return Err(RuntimeError::Invariant( + "async module callable returned a non-Promise", + )); + } + }, + Completion::Throw(value) => { + runtime.release_jsvalue(value)?; + return Err(RuntimeError::Invariant( "async module callable threw instead of returning a Promise", - )), - Completion::Return(_) => Err(RuntimeError::Invariant( - "async module callable returned a non-Promise", - )), - }; + )); + } }; let snapshot = runtime .0 @@ -144,8 +161,8 @@ fn inspect_sync(runtime: &Runtime, completion: Completion) -> Result Ok(Completion::Return(result)), - PromiseState::Rejected => Ok(Completion::Throw(result)), + PromiseState::Fulfilled => Ok(Completion::Return(runtime.into_jsvalue(result)?)), + PromiseState::Rejected => Ok(Completion::Throw(runtime.into_jsvalue(result)?)), PromiseState::Pending => Err(RuntimeError::Invariant( "synchronous module body retained a pending Promise", )), diff --git a/src/engine/modules/callback.rs b/src/engine/modules/callback.rs index 2570ca00..22dae63e 100644 --- a/src/engine/modules/callback.rs +++ b/src/engine/modules/callback.rs @@ -8,7 +8,7 @@ use crate::engine::heap::{ ContextId, InternalCallableData, ModuleId, RawModuleRef, RawModuleTransition, RawValue, }; use crate::engine::object::CallableRef; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -19,7 +19,7 @@ pub(crate) enum CallbackStep { Complete(Completion), Call { callable: CallableRef, - value: Value, + value: JsValue, resume: Box, }, Body { @@ -52,6 +52,14 @@ enum Mode { }, DynamicSettled, } + +/// Balance the boundary conversion's producer edge exactly once. `None` after +/// the first call marks the edge as already transferred or released. +fn release_conversion_probe(runtime: &Runtime, probe: &mut Option) { + if let Some(probe) = probe.take() { + runtime.release_converted_value_edge(&probe); + } +} pub(crate) struct CallbackResume { runtime: Runtime, realm: ContextId, @@ -83,18 +91,21 @@ impl CallbackStep { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(runtime); return Err(RuntimeError::Invariant( "module evaluation callback received a constructor invocation", )); }; - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "module evaluation callback argv was not padded", - ))?; + invocation.release(runtime)?; + let argument = match arguments.readable.first() { + Some(value) => runtime.root_value(value)?, + None => { + return Err(RuntimeError::Invariant( + "module evaluation callback argv was not padded", + )); + } + }; let active = runtime.active_function()?; let internal = runtime .0 @@ -127,18 +138,21 @@ impl CallbackStep { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(runtime); return Err(RuntimeError::Invariant( "dynamic import handler received a constructor invocation", )); }; - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "dynamic import handler argv was not padded", - ))?; + invocation.release(runtime)?; + let argument = match arguments.readable.first() { + Some(value) => runtime.root_value(value)?, + None => { + return Err(RuntimeError::Invariant( + "dynamic import handler argv was not padded", + )); + } + }; let active = runtime.active_function()?; let internal = runtime .0 @@ -178,7 +192,7 @@ impl CallbackStep { let callable = runtime.dynamic_import_settler(target)?; Ok(Self::Call { callable, - value, + value: runtime.into_jsvalue(value)?, resume: Box::new(CallbackResume { runtime: runtime.clone(), realm, @@ -194,7 +208,7 @@ impl CallbackStep { ) -> Result { match runtime.module_record(module)?.evaluation { ModuleEvaluationState::Errored(_) => { - return Ok(Self::Complete(Completion::Return(Value::Undefined))); + return Ok(Self::Complete(Completion::Return(JsValue::Undefined))); } ModuleEvaluationState::EvaluatingAsync => {} _ => { @@ -218,11 +232,11 @@ impl CallbackStep { { return Ok(Self::Call { callable, - value: Value::Undefined, + value: JsValue::Undefined, resume, }); } - resume.resume(Completion::Return(Value::Undefined)) + resume.resume(Completion::Return(JsValue::Undefined)) } fn reject( runtime: &Runtime, @@ -232,10 +246,20 @@ impl CallbackStep { ) -> Result { runtime.validate_value_domain(&reason, "async module rejection")?; let raw = runtime.raw_property_value(&reason)?; + // Clone duplicates only the handle; the probe keeps the producer edge + // accountable until the first error record stores the value. + let raw_probe = raw.clone(); + let root = match runtime.root_module(module) { + Ok(root) => root, + Err(error) => { + runtime.release_converted_value_edge(&raw_probe); + return Err(error); + } + }; Box::new(CallbackResume { runtime: runtime.clone(), realm, - root: runtime.root_module(module)?, + root, mode: Mode::Reject { reason, raw, @@ -268,9 +292,9 @@ impl CallbackResume { match &mut self.mode { Mode::DynamicSettled => { return match completion { - Completion::Return(_) => { - Ok(CallbackStep::Complete(Completion::Return(Value::Undefined))) - } + Completion::Return(_) => Ok(CallbackStep::Complete(Completion::Return( + JsValue::Undefined, + ))), Completion::Throw(_) => Err(RuntimeError::Invariant( "intrinsic dynamic import resolving function threw", )), @@ -296,7 +320,7 @@ impl CallbackResume { return self.advance(); } match completion { - Completion::Return(Value::Undefined) => { + Completion::Return(JsValue::Undefined) => { self.runtime.transition_module_record( module, RawModuleTransition::FinishAsyncEvaluation, @@ -307,12 +331,13 @@ impl CallbackResume { { return Ok(CallbackStep::Call { callable, - value: Value::Undefined, + value: JsValue::Undefined, resume: self, }); } } Completion::Throw(reason) => { + let reason = self.runtime.root_and_release_jsvalue(reason)?; let step = CallbackStep::reject(&self.runtime, self.realm, module, reason)?; return Ok(CallbackStep::Nested { @@ -353,7 +378,9 @@ impl CallbackResume { resume: self, }); } - Ok(CallbackStep::Complete(Completion::Return(Value::Undefined))) + Ok(CallbackStep::Complete(Completion::Return( + JsValue::Undefined, + ))) } Mode::Reject { reason, @@ -361,16 +388,28 @@ impl CallbackResume { pending, parents, } => { + // The conversion minted at `CallbackStep::reject` carries one + // producer edge. The first published record retains its own + // copy edge, after which the producer edge is released once; + // every exit before that publication releases it immediately. + let mut conversion_probe = Some(raw.clone()); while let Some(id) = pending.pop() { let current = RawModuleRef { cache: self.root.raw.cache, module: id, }; - let record = self.runtime.module_record(current)?; + let record = match self.runtime.module_record(current) { + Ok(record) => record, + Err(error) => { + release_conversion_probe(&self.runtime, &mut conversion_probe); + return Err(error); + } + }; match record.evaluation { ModuleEvaluationState::Errored(_) => continue, ModuleEvaluationState::EvaluatingAsync => {} _ => { + release_conversion_probe(&self.runtime, &mut conversion_probe); return Err(RuntimeError::Invariant( "async module rejection reached an inactive ancestor", )); @@ -380,7 +419,14 @@ impl CallbackResume { let mut state = self.runtime.0.state.borrow_mut(); let retained_atoms = match raw { RawValue::Symbol(atom) => { - Runtime::retain_module_atoms(&mut state, vec![*atom])? + match Runtime::retain_module_atoms(&mut state, vec![*atom]) { + Ok(atoms) => atoms, + Err(error) => { + drop(state); + release_conversion_probe(&self.runtime, &mut conversion_probe); + return Err(error); + } + } } _ => Vec::new(), }; @@ -388,10 +434,16 @@ impl CallbackResume { .heap .publish_loaded_module_async_error(current, raw.clone()) { - state.release_atoms(retained_atoms)?; + let release_result = state.release_atom_indices(retained_atoms); + drop(state); + release_conversion_probe(&self.runtime, &mut conversion_probe); + release_result?; return Err(error.into()); } drop(state); + // The record retained its own copy edge; the producer + // edge is no longer needed. + release_conversion_probe(&self.runtime, &mut conversion_probe); // Publish this node, settle it, then visit parents in reference order. if let Some(callable) = self .runtime @@ -400,13 +452,18 @@ impl CallbackResume { *parents = next_parents; return Ok(CallbackStep::Call { callable, - value: reason.clone(), + value: self.runtime.unroot_value(reason)?, resume: self, }); } pending.extend(next_parents.into_iter().rev()); } - Ok(CallbackStep::Complete(Completion::Return(Value::Undefined))) + // Every ancestor was already errored: no record consumed the + // value, so its producer edge dies with this walk. + release_conversion_probe(&self.runtime, &mut conversion_probe); + Ok(CallbackStep::Complete(Completion::Return( + JsValue::Undefined, + ))) } } } diff --git a/src/engine/modules/evaluation.rs b/src/engine/modules/evaluation.rs index 648365dd..2361a2ce 100644 --- a/src/engine/modules/evaluation.rs +++ b/src/engine/modules/evaluation.rs @@ -8,7 +8,7 @@ use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::builtins::promise::RootedPromiseCapability; use crate::engine::heap::{ContextId, RawModuleRef, RawModuleTransition}; use crate::engine::object::{CallableRef, ObjectRef}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; pub(crate) enum EvaluationStep { @@ -19,7 +19,7 @@ pub(crate) enum EvaluationStep { }, Call { callable: CallableRef, - value: Value, + value: JsValue, resume: Box, }, } @@ -74,8 +74,9 @@ impl EvaluationStep { | ModuleEvaluationState::Evaluated | ModuleEvaluationState::Errored(_) => { ObjectRef::from_borrowed_handle(runtime.clone(), promise) - .map(|promise| Self::Complete(Completion::Return(Value::Object(promise)))) - .map_err(Into::into) + .map_err(RuntimeError::from) + .and_then(|promise| runtime.into_jsvalue(Value::Object(promise))) + .map(|value| Self::Complete(Completion::Return(value))) } ModuleEvaluationState::Unevaluated => Err(RuntimeError::Invariant( "module retained an unsettled Promise before evaluation", @@ -98,12 +99,11 @@ impl EvaluationStep { "module cycle-root evaluation previously failed inside the engine", )); } - if record.link_status != ModuleLinkStatus::Linked { + if !matches!(record.link_status, ModuleLinkStatus::Linked) { return Err(RuntimeError::Invariant( "module evaluation Promise was requested before linking", )); } - let capability = runtime.new_default_promise_capability(initiating_realm)?; let promise = capability.promise.clone(); runtime @@ -142,9 +142,9 @@ impl EvaluationStep { ModuleEvaluationState::Errored(reason) => { resume.settle(false, runtime.root_raw_value(&reason)?) } - ModuleEvaluationState::EvaluatingAsync => { - Ok(Self::Complete(Completion::Return(Value::Object(promise)))) - } + ModuleEvaluationState::EvaluatingAsync => Ok(Self::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(promise))?, + ))), ModuleEvaluationState::Evaluating => Err(RuntimeError::Invariant( "module evaluation Promise was requested during evaluation", )), @@ -166,10 +166,18 @@ impl EvaluationStep { .map_err(RuntimeError::Engine)?; match completion { - Completion::Return(Value::Object(promise)) => Ok(promise), - _ => Err(RuntimeError::Invariant( - "module evaluation did not return a Promise", - )), + Completion::Return(value) => match runtime.root_and_release_jsvalue(value)? { + Value::Object(promise) => Ok(promise), + _ => Err(RuntimeError::Invariant( + "module evaluation did not return a Promise", + )), + }, + Completion::Throw(value) => { + runtime.release_jsvalue(value)?; + Err(RuntimeError::Invariant( + "module evaluation did not return a Promise", + )) + } } } } @@ -188,7 +196,7 @@ impl EvaluationResume { }; Ok(EvaluationStep::Call { callable, - value, + value: self.runtime.into_jsvalue(value)?, resume: self, }) } @@ -199,7 +207,8 @@ impl EvaluationResume { if self.settling { return match completion { Completion::Return(_) => Ok(EvaluationStep::Complete(Completion::Return( - Value::Object(self.capability.promise.clone()), + self.runtime + .into_jsvalue(Value::Object(self.capability.promise.clone()))?, ))), Completion::Throw(_) => Err(RuntimeError::Invariant( "intrinsic module Promise resolving function threw", @@ -257,9 +266,12 @@ impl EvaluationResume { } self.armed = false; match self.runtime.module_record(self.root.raw)?.evaluation { - ModuleEvaluationState::EvaluatingAsync => Ok(EvaluationStep::Complete( - Completion::Return(Value::Object(self.capability.promise.clone())), - )), + ModuleEvaluationState::EvaluatingAsync => { + Ok(EvaluationStep::Complete(Completion::Return( + self.runtime + .into_jsvalue(Value::Object(self.capability.promise.clone()))?, + ))) + } ModuleEvaluationState::Evaluated => self.settle(true, Value::Undefined), ModuleEvaluationState::Errored(reason) => { let reason = self.runtime.root_raw_value(&reason)?; @@ -438,7 +450,7 @@ impl EvaluationResume { dfs, frames, frame, - Completion::Return(Value::Undefined), + Completion::Return(JsValue::Undefined), )?; continue; } @@ -468,7 +480,7 @@ impl EvaluationResume { completion: Completion, ) -> Result<(), RuntimeError> { match completion { - Completion::Return(Value::Undefined) => { + Completion::Return(JsValue::Undefined) => { let entry = dfs.entries.get(&frame.module.module).copied().ok_or( RuntimeError::Invariant("evaluated module lost its DFS entry"), )?; @@ -520,6 +532,7 @@ impl EvaluationResume { )); } Completion::Throw(exception) => { + let exception = runtime.root_and_release_jsvalue(exception)?; if dfs.exception.replace(exception).is_some() { return Err(RuntimeError::Invariant( "module evaluation recorded more than one exception", diff --git a/src/engine/modules/import.rs b/src/engine/modules/import.rs index 1cf9538a..f5a8ead4 100644 --- a/src/engine/modules/import.rs +++ b/src/engine/modules/import.rs @@ -8,13 +8,13 @@ use crate::engine::code::{ }; use crate::engine::heap::ContextId; use crate::engine::object::{CallableRef, ObjectRef, PropertyKey}; -use crate::engine::value::{JsString, Value, conversion::NativeConversion}; +use crate::engine::value::{JsString, JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::Completion; pub(crate) enum ImportStep { Complete(Completion), String { - value: Value, + value: JsValue, resume: Box, }, Read { @@ -33,7 +33,7 @@ pub(crate) enum ImportStep { }, Call { callable: CallableRef, - reason: Value, + reason: JsValue, resume: Box, }, } @@ -71,7 +71,7 @@ impl ImportStep { let base_name = runtime.active_script_or_module_name()?; let capability = runtime.new_default_promise_capability(realm)?; Ok(Self::String { - value: specifier, + value: runtime.into_jsvalue(specifier)?, resume: Box::new(ImportResume { realm, base_name, @@ -89,13 +89,17 @@ impl ImportStep { } } impl ImportResume { - fn reject(mut self: Box, reason: Value) -> ImportStep { + fn reject( + mut self: Box, + runtime: &Runtime, + reason: Value, + ) -> Result { self.phase = Phase::Reject; - ImportStep::Call { + Ok(ImportStep::Call { callable: self.capability.reject.clone(), - reason, + reason: runtime.into_jsvalue(reason)?, resume: self, - } + }) } fn type_error( self: Box, @@ -103,7 +107,7 @@ impl ImportResume { message: &str, ) -> Result { let reason = runtime.new_native_error(self.realm, NativeErrorKind::Type, message)?; - Ok(self.reject(reason)) + self.reject(runtime, reason) } fn enqueue( mut self: Box, @@ -120,9 +124,9 @@ impl ImportResume { specifier, attributes, )?; - Ok(ImportStep::Complete(Completion::Return(Value::Object( - self.capability.promise, - )))) + Ok(ImportStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(self.capability.promise))?, + ))) } pub(crate) fn resume( mut self: Box, @@ -132,7 +136,7 @@ impl ImportResume { if matches!(self.phase, Phase::Reject) { return match completion { Completion::Return(_) => Ok(ImportStep::Complete(Completion::Return( - Value::Object(self.capability.promise), + runtime.into_jsvalue(Value::Object(self.capability.promise))?, ))), Completion::Throw(_) => Err(RuntimeError::Invariant( "intrinsic dynamic import reject function threw", @@ -140,8 +144,11 @@ impl ImportResume { }; } let value = match completion { - Completion::Throw(reason) => return Ok(self.reject(reason)), - Completion::Return(value) => value, + Completion::Throw(reason) => { + let reason = runtime.root_and_release_jsvalue(reason)?; + return self.reject(runtime, reason); + } + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, }; match std::mem::replace(&mut self.phase, Phase::With) { Phase::Specifier(options) => { @@ -203,7 +210,7 @@ impl ImportResume { ) -> Result { let keys = match result { NativeConversion::Value(keys) => keys, - NativeConversion::Throw(reason) => return Ok(self.reject(reason)), + NativeConversion::Throw(reason) => return self.reject(runtime, reason), }; if !matches!(self.phase, Phase::Descriptors) { return Err(RuntimeError::Invariant( @@ -231,7 +238,7 @@ impl ImportResume { ) -> Result { let enumerable = match result { NativeConversion::Value(value) => value, - NativeConversion::Throw(reason) => return Ok(self.reject(reason)), + NativeConversion::Throw(reason) => return self.reject(runtime, reason), }; if !matches!(self.phase, Phase::Descriptors) { return Err(RuntimeError::Invariant( @@ -276,7 +283,7 @@ impl ImportResume { } match runtime.check_dynamic_import_attributes(self.realm, &self.entries)? { NativeConversion::Value(()) => {} - NativeConversion::Throw(reason) => return Ok(self.reject(reason)), + NativeConversion::Throw(reason) => return self.reject(runtime, reason), } let entries = std::mem::take(&mut self.entries).into_boxed_slice(); self.enqueue(runtime, ModuleImportAttributes::Present(entries)) diff --git a/src/engine/modules/link.rs b/src/engine/modules/link.rs index 141db1ce..f93a93bd 100644 --- a/src/engine/modules/link.rs +++ b/src/engine/modules/link.rs @@ -3,7 +3,7 @@ use super::{ModuleBytecodeRef, ModuleDfsFrame, ModuleLinkDfs, ModuleLinkStatus}; use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::heap::{ContextId, RawModuleLinkRealm, RawModuleRef, RawModuleTransition}; use crate::engine::object::CallableRef; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::Completion; pub(crate) enum LinkStep { @@ -33,7 +33,7 @@ impl LinkStep { runtime.prepare_module_instance(module, initiating_realm)?; match runtime.module_record(module)?.link_status { ModuleLinkStatus::Linked => { - return Ok(LinkStep::Complete(Completion::Return(Value::Undefined))); + return Ok(LinkStep::Complete(Completion::Return(JsValue::Undefined))); } ModuleLinkStatus::Linking => { return Err(RuntimeError::Invariant( @@ -139,7 +139,7 @@ impl LinkResume { dfs, frames, frame, - Completion::Return(Value::Undefined), + Completion::Return(JsValue::Undefined), )?; } if !dfs.stack.is_empty() { @@ -148,7 +148,7 @@ impl LinkResume { )); } self.armed = false; - Ok(LinkStep::Complete(Completion::Return(Value::Undefined))) + Ok(LinkStep::Complete(Completion::Return(JsValue::Undefined))) } fn finish_frame( runtime: &Runtime, @@ -158,7 +158,7 @@ impl LinkResume { completion: Completion, ) -> Result<(), RuntimeError> { match completion { - Completion::Return(Value::Undefined) => { + Completion::Return(JsValue::Undefined) => { let entry = dfs .entries .get(&frame.module.module) @@ -174,7 +174,10 @@ impl LinkResume { cache: frame.module.cache, module: member, }; - if runtime.module_record(member)?.link_status != ModuleLinkStatus::Linking { + if !matches!( + runtime.module_record(member)?.link_status, + ModuleLinkStatus::Linking + ) { return Err(RuntimeError::Invariant( "module link SCC contained a non-linking member", )); @@ -202,12 +205,15 @@ impl LinkResume { )); } Completion::Throw(exception) => { - runtime.set_pending_exception(exception)?; + runtime.set_pending_exception_jsvalue(exception)?; return Err(RuntimeError::Exception); } } - if runtime.module_record(frame.module)?.link_status == ModuleLinkStatus::Linking { + if matches!( + runtime.module_record(frame.module)?.link_status, + ModuleLinkStatus::Linking + ) { let dependency_ancestor = dfs .entries .get(&frame.module.module) @@ -262,7 +268,7 @@ impl Drop for LinkResume { if self .runtime .module_record(member) - .is_ok_and(|record| record.link_status == ModuleLinkStatus::Linking) + .is_ok_and(|record| matches!(record.link_status, ModuleLinkStatus::Linking)) { let _ = self .runtime @@ -283,7 +289,9 @@ pub(crate) fn resume_reply( .ok_or(RuntimeError::Invariant( "module link exception has no pending value", ))?; - Ok(LinkStep::Complete(Completion::Throw(reason))) + Ok(LinkStep::Complete(Completion::Throw( + runtime.into_jsvalue(reason)?, + ))) } result => result, } diff --git a/src/engine/modules/mod.rs b/src/engine/modules/mod.rs index 22c579bf..b986ee45 100644 --- a/src/engine/modules/mod.rs +++ b/src/engine/modules/mod.rs @@ -17,7 +17,7 @@ use crate::engine::api::context::Context; 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::atom::Atom; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::{DynamicImportHandlerKind, ModuleEvaluationKind}; use crate::engine::code::bytecode_publish; @@ -55,7 +55,7 @@ use crate::engine::object::{ WellKnownSymbol, }; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, JsStringError, Value}; +use crate::engine::value::{JsString, JsStringError, JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{NativeArguments, NativeInvocation}; use crate::engine::vm::frames::ExplicitBacktraceLocation; @@ -487,9 +487,15 @@ struct ModuleResolvedBinding { target: ModuleResolvedBindingTarget, } +/// `RawModuleRef` deliberately carries no `PartialEq`; identity is the pair of +/// plain arena handles, compared here at the module-language boundary. +fn same_raw_module(left: RawModuleRef, right: RawModuleRef) -> bool { + left.cache == right.cache && left.module == right.module +} + impl ModuleResolvedBinding { fn has_same_identity(&self, other: &Self) -> bool { - if self.module != other.module { + if !same_raw_module(self.module, other.module) { return false; } match (&self.target, &other.target) { @@ -729,7 +735,7 @@ impl ModuleBytecodeRef { impl PartialEq for ModuleBytecodeRef { fn eq(&self, other: &Self) -> bool { - self.runtime.is_same_runtime(&other.runtime) && self.raw == other.raw + self.runtime.is_same_runtime(&other.runtime) && same_raw_module(self.raw, other.raw) } } @@ -1056,7 +1062,7 @@ impl Runtime { }) } - fn module_value_atoms(record: &ModuleRecord) -> Vec { + fn module_value_atoms(record: &ModuleRecord) -> Vec { let mut atoms = Vec::with_capacity(2); if let ModuleRecordBody::Json { default_value: RawValue::Symbol(atom) | RawValue::Private(atom), @@ -1075,7 +1081,7 @@ impl Runtime { fn module_value_atom_delta( current: &ModuleRecord, replacement: &ModuleRecord, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { let mut old = Self::module_value_atoms(current); let new = Self::module_value_atoms(replacement); let mut added = Vec::with_capacity(new.len()); @@ -1091,11 +1097,11 @@ impl Runtime { fn retain_module_atoms( state: &mut RuntimeState, - atoms: Vec, - ) -> Result, RuntimeError> { + atoms: Vec, + ) -> Result, RuntimeError> { for (retained, &atom) in atoms.iter().enumerate() { - if let Err(error) = state.atoms.retain(atom) { - state.release_atoms(atoms[..retained].iter().copied())?; + if let Err(error) = state.atoms.retain_index(atom) { + state.release_atom_indices(atoms[..retained].iter().copied())?; return Err(error.into()); } } @@ -1114,7 +1120,7 @@ impl Runtime { Ok(module) => Ok(module), Err(error) => { state - .release_atoms(retained_atoms) + .release_atom_indices(retained_atoms) .expect("loaded-module atom rollback failed after rejected publication"); Err(error.into()) } @@ -1129,7 +1135,11 @@ impl Runtime { let mut state = self.0.state.borrow_mut(); let current = state.heap.loaded_module(module)?; let (added_atoms, removed_atoms) = Self::module_value_atom_delta(¤t, &replacement); - state.preflight_atom_releases(&removed_atoms)?; + let mut removed_branded = Vec::with_capacity(removed_atoms.len()); + for &index in &removed_atoms { + removed_branded.push(state.atoms.brand(index)?); + } + state.preflight_atom_releases(&removed_branded)?; let retained_atoms = Self::retain_module_atoms(&mut state, added_atoms)?; match state.heap.replace_loaded_module(module, replacement) { Ok(cleanup) => { @@ -1139,7 +1149,7 @@ impl Runtime { } Err(error) => { state - .release_atoms(retained_atoms) + .release_atom_indices(retained_atoms) .expect("loaded-module atom rollback failed after rejected replacement"); Err(error.into()) } @@ -1235,7 +1245,11 @@ impl Runtime { .iter() .flat_map(Self::module_value_atoms) .collect::>(); - state.preflight_atom_releases(&removed_atoms)?; + let mut removed_branded = Vec::with_capacity(removed_atoms.len()); + for &index in &removed_atoms { + removed_branded.push(state.atoms.brand(index)?); + } + state.preflight_atom_releases(&removed_branded)?; let cleanup = state.heap.unpublish_loaded_modules(cache, &doomed)?; debug_assert!(cleanup.atoms.starts_with(&removed_atoms)); state.apply_committed_cleanup(cleanup); @@ -1294,7 +1308,9 @@ impl Runtime { ) })); match outcome { - Ok(Ok(ModuleCompilation::Published(module))) if module == parsing_module => { + Ok(Ok(ModuleCompilation::Published(module))) + if same_raw_module(module, parsing_module) => + { Ok(ModuleCompilation::Published(module)) } Ok(Ok(ModuleCompilation::Published(_))) => { @@ -1584,7 +1600,7 @@ impl Runtime { let popped = stack.pop().ok_or(RuntimeError::Invariant( "module resolution stack unexpectedly became empty", ))?; - if popped.module != completed { + if !same_raw_module(popped.module, completed) { return Err(RuntimeError::Invariant( "module resolution stack changed during record publication", )); @@ -1983,6 +1999,9 @@ impl Runtime { ) -> Result { self.validate_value_domain(&default_value, "JSON module value")?; let raw_default_value = self.raw_property_value(&default_value)?; + // Clone duplicates only the handle; the probe keeps the producer edge + // accountable after the record consumes the value. + let default_value_probe = raw_default_value.clone(); let record = ModuleRecord { name, body: ModuleRecordBody::Json { @@ -2016,7 +2035,12 @@ impl Runtime { link_realm: None, compile_realm: realm, }; - let published = self.publish_module_record(realm, record)?; + let published = self.publish_module_record(realm, record); + // `publish_module_record` retained the record's own node edge on + // success; a rejected publication never stores the value. Either way + // the boundary conversion's producer edge must be balanced here. + self.release_converted_value_edge(&default_value_probe); + let published = published?; drop(default_value); Ok(published) } @@ -2240,7 +2264,7 @@ impl Runtime { )); } let meta = self.get_or_create_module_import_meta(module)?; - Some(self.new_var_ref( + Some(self.new_var_ref_rooted( Value::Object(meta), true, true, @@ -2276,7 +2300,7 @@ impl Runtime { // detached VarRef per local C/synthetic export. Its initial value // is `undefined`; the module initializer writes the JSON value at // evaluation time. - slots.push(Some(self.new_var_ref( + slots.push(Some(self.new_var_ref_rooted( Value::Undefined, false, false, @@ -2626,7 +2650,7 @@ impl Runtime { ) -> Result { match self.module_record(module)?.namespace { ModuleNamespaceState::Building(object) => { - if !created.contains(&module) { + if !created.iter().any(|entry| same_raw_module(*entry, module)) { return Err(RuntimeError::Invariant( "module namespace cache retained a stale Building record", )); @@ -2672,12 +2696,23 @@ impl Runtime { } let tag = PropertyKey::from(self.well_known_symbol(WellKnownSymbol::ToStringTag)); - self.store_property_slot( + // A genuine value-producing boundary: mint the string node outside the + // store borrow, then hand its producer edge to the transactional store. + let tag_string = { + let mut state = self.0.state.borrow_mut(); + state + .heap + .allocate_string(JsString::from_static("Module"))? + }; + let tag_raw = RawValue::String(tag_string); + let stored = self.store_property_slot( &namespace, &tag, PropertyFlags::data(false, false, false), - PropertySlot::Data(RawValue::String(JsString::from_static("Module"))), - )?; + PropertySlot::Data(tag_raw.clone()), + ); + self.release_converted_value_edge(&tag_raw); + stored?; self.transition_module_record( module, RawModuleTransition::FinishNamespace(namespace.object_id()), @@ -2749,7 +2784,7 @@ impl Runtime { let target = self.module_dependency(binding.module, *request)?; let namespace = self.build_module_namespace(target, realm, namespace_transaction)?; - return self.new_var_ref( + return self.new_var_ref_rooted( Value::Object(namespace), true, true, @@ -2840,7 +2875,7 @@ impl Runtime { realm, namespace_transaction, )?; - return self.new_var_ref( + return self.new_var_ref_rooted( Value::Object(namespace), true, true, @@ -3004,7 +3039,7 @@ impl Runtime { "namespace import has no preallocated declaration cell", ))?; let slot = VarRefRoot::from_borrowed_handle(self.clone(), slot)?; - self.write_var_ref(&slot, Value::Object(namespace))?; + self.write_var_ref(&slot, self.into_jsvalue(Value::Object(namespace))?)?; continue; } ModuleImportName::Name(import_name) => { @@ -3107,7 +3142,10 @@ impl Runtime { module: RawModuleRef, dfs: &mut ModuleLinkDfs, ) -> Result { - if self.module_record(module)?.link_status != ModuleLinkStatus::Unlinked { + if !matches!( + self.module_record(module)?.link_status, + ModuleLinkStatus::Unlinked + ) { return Err(RuntimeError::Invariant( "link DFS entered a module which was not unlinked", )); @@ -3157,9 +3195,9 @@ impl Runtime { ) .map_err(RuntimeError::Engine)?; match completion { - Completion::Return(Value::Undefined) => Ok(()), + Completion::Return(JsValue::Undefined) => Ok(()), Completion::Throw(value) => { - self.set_pending_exception(value)?; + self.set_pending_exception_jsvalue(value)?; Err(RuntimeError::Exception) } _ => Err(RuntimeError::Invariant( @@ -3370,7 +3408,7 @@ impl Runtime { crate::engine::vm::entry::call(self, realm, &target, Value::Undefined, &[value])?; match completion { - Completion::Return(_) => Ok(Completion::Return(Value::Undefined)), + Completion::Return(_) => Ok(Completion::Return(JsValue::Undefined)), Completion::Throw(_) => Err(RuntimeError::Invariant( "intrinsic dynamic import resolving function threw", )), @@ -3472,7 +3510,7 @@ impl Runtime { resolve, reject, )? { - NativeConversion::Value(()) => Ok(Completion::Return(Value::Undefined)), + NativeConversion::Value(()) => Ok(Completion::Return(JsValue::Undefined)), NativeConversion::Throw(value) => { // `JS_LoadModuleInternal` frees the abrupt `js_promise_then` // result and the surrounding load job still returns @@ -3480,7 +3518,7 @@ impl Runtime { // while the caller-facing import Promise deliberately stays // pending in this edge case. self.set_pending_exception(value)?; - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } } } @@ -3494,18 +3532,29 @@ impl Runtime { ) -> Result<(), RuntimeError> { self.validate_value_domain(exception, "module evaluation exception")?; let raw = self.raw_property_value(exception)?; + // Clone duplicates only the handle; the probe keeps the producer edge + // accountable after the records and the pending-exception slot consume + // the value. + let conversion_probe = raw.clone(); let mut evaluating = Vec::with_capacity(active.len()); for &id in active { - if matches!( - self.module_record(RawModuleRef { cache, module: id })? - .evaluation, - ModuleEvaluationState::Evaluating - ) { + let record = match self.module_record(RawModuleRef { cache, module: id }) { + Ok(record) => record, + Err(error) => { + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } + }; + if matches!(record.evaluation, ModuleEvaluationState::Evaluating) { evaluating.push(id); } } let mut state = self.0.state.borrow_mut(); - state.retain_raw_root(&raw)?; + if let Err(error) = state.retain_raw_root(&raw) { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } let retained_atoms = match &raw { RawValue::Symbol(atom) => { let count = evaluating.len(); @@ -3526,9 +3575,11 @@ impl Runtime { .publish_loaded_module_errors(cache, &evaluating, cycle_root, raw.clone()) { state - .release_atoms(retained_atoms) + .release_atom_indices(retained_atoms) .expect("module evaluation error atom rollback failed"); state.release_owned_raw_root_committed(raw); + drop(state); + self.release_converted_value_edge(&conversion_probe); return Err(error.into()); } // One extra owned occurrence was prepared with the cache batch, so @@ -3537,6 +3588,10 @@ impl Runtime { if let Some(previous) = previous { state.release_owned_raw_root_committed(previous); } + drop(state); + // The records and the pending-exception slot retained their own edges; + // the boundary conversion's producer edge is no longer needed. + self.release_converted_value_edge(&conversion_probe); Ok(()) } diff --git a/src/engine/modules/namespace.rs b/src/engine/modules/namespace.rs index ace890b2..ca86fb7e 100644 --- a/src/engine/modules/namespace.rs +++ b/src/engine/modules/namespace.rs @@ -8,7 +8,7 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::PropertyKeyKind; +use crate::engine::atom::{AtomIdx, PropertyKeyKind}; use crate::engine::heap::{ObjectData, ObjectKind, PropertySlot}; use crate::engine::object::{ @@ -25,8 +25,10 @@ impl Runtime { if !object.belongs_to(self) { return Err(RuntimeError::WrongRuntime("object")); } - Ok(self.0.state.borrow().heap.object(object.object_id())?.kind - == ObjectKind::ModuleNamespace) + Ok(matches!( + self.0.state.borrow().heap.object(object.object_id())?.kind, + ObjectKind::ModuleNamespace + )) } /// Allocate the null-prototype, already non-extensible namespace shell. @@ -69,7 +71,7 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(object.object_id())?; let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(false); }; Ok(matches!( @@ -96,8 +98,11 @@ impl Runtime { let object = state.heap.object(object.object_id())?; let mut atoms = Vec::new(); for entry in state.heap.shape(object.shape)?.entries() { - if state.atoms.property_key_kind(entry.atom)? != PropertyKeyKind::Private { - atoms.push(entry.atom); + // Public exit boundary: re-brand the stored unbranded index + // before handing the atom to `PropertyKey` construction. + let atom = state.atoms.brand(entry.atom)?; + if state.atoms.property_key_kind(atom)? != PropertyKeyKind::Private { + atoms.push(atom); } } atoms diff --git a/src/engine/modules/tests.rs b/src/engine/modules/tests.rs index 256598a9..ccf80f59 100644 --- a/src/engine/modules/tests.rs +++ b/src/engine/modules/tests.rs @@ -797,6 +797,14 @@ fn assert_script_true(context: &mut Context, source: &str) { assert_eq!(context.eval(source).unwrap(), Value::Bool(true)); } +#[track_caller] +fn assert_returned_completion(runtime: &Runtime, completion: Completion, expected: Value) { + let Completion::Return(value) = completion else { + panic!("expected Completion::Return"); + }; + assert_eq!(runtime.root_and_release_jsvalue(value).unwrap(), expected); +} + fn eval_dynamic_import(context: &mut Context, source: &str, filename: &str) -> ObjectRef { let Value::Object(promise) = context.eval_with_filename(source, filename).unwrap() else { panic!("dynamic import did not return an object"); diff --git a/src/engine/modules/tests/construction_tests.rs b/src/engine/modules/tests/construction_tests.rs index cbd25f22..1f03fe9b 100644 --- a/src/engine/modules/tests/construction_tests.rs +++ b/src/engine/modules/tests/construction_tests.rs @@ -1074,7 +1074,7 @@ fn referenced_failed_parsing_identity_is_aborted_without_quickjs_aba() { .compile_module_with_filename("globalThis.__parseCacheSafeRetry = 42;", "outer.js") .unwrap(); assert_eq!(retry.raw.module.0, 3); - assert_eq!( + assert!(matches!( runtime .0 .state @@ -1082,8 +1082,8 @@ fn referenced_failed_parsing_identity_is_aborted_without_quickjs_aba() { .heap .first_loaded_module(context.realm, &JsString::from_static("outer.js")) .unwrap(), - Some(retry.raw) - ); + Some(found) if found.cache == retry.raw.cache && found.module == retry.raw.module + )); assert_eq!( context.link_module(&probe), Err(RuntimeError::AbortedModule) diff --git a/src/engine/modules/tests/dynamic_import.rs b/src/engine/modules/tests/dynamic_import.rs index d15ca8cd..b96215c3 100644 --- a/src/engine/modules/tests/dynamic_import.rs +++ b/src/engine/modules/tests/dynamic_import.rs @@ -39,11 +39,12 @@ fn dynamic_import_load_and_finish_are_distinct_fifo_jobs_with_gc_roots() { panic!("dynamic import did not fulfill with a namespace object"); }; let answer = runtime.intern_property_key("answer").unwrap(); - assert_eq!( + assert_returned_completion( + &runtime, runtime .get_property_in_realm(context.realm, &namespace, &answer) .unwrap(), - Completion::Return(Value::Int(42)) + Value::Int(42), ); assert!(!runtime.is_job_pending()); #[cfg(feature = "profiling")] @@ -137,11 +138,12 @@ fn dynamic_import_waits_for_a_pending_tla_evaluation_and_reuses_it() { }; assert_eq!(first_namespace.object_id(), second_namespace.object_id()); let answer = runtime.intern_property_key("answer").unwrap(); - assert_eq!( + assert_returned_completion( + &runtime, runtime .get_property_in_realm(context.realm, &first_namespace, &answer) .unwrap(), - Completion::Return(Value::Int(42)) + Value::Int(42), ); assert_script_true( &mut context, diff --git a/src/engine/modules/tests/dynamic_import_cache.rs b/src/engine/modules/tests/dynamic_import_cache.rs index c69c415a..f0a2f50b 100644 --- a/src/engine/modules/tests/dynamic_import_cache.rs +++ b/src/engine/modules/tests/dynamic_import_cache.rs @@ -23,11 +23,12 @@ fn dynamic_import_load_job_samples_the_current_loader() { panic!("sampled dynamic import did not return a namespace"); }; let source = runtime.intern_property_key("source").unwrap(); - assert_eq!( + assert_returned_completion( + &runtime, runtime .get_property_in_realm(context.realm, &namespace, &source) .unwrap(), - Completion::Return(Value::Int(2)) + Value::Int(2), ); } @@ -156,7 +157,10 @@ fn dynamic_import_reuses_cycle_root_rejection_promise_and_tracker_history() { assert!(b.evaluation_promise.is_none()); (cycle_a, cycle_b, a.evaluation_promise.unwrap()) }; - assert_ne!(cycle_a, cycle_b); + assert!( + cycle_a.cache != cycle_b.cache || cycle_a.module != cycle_b.module, + "cycle records must be distinct identities" + ); let second = eval_dynamic_import( &mut context, diff --git a/src/engine/modules/tests/evaluation.rs b/src/engine/modules/tests/evaluation.rs index c68292b1..b979e736 100644 --- a/src/engine/modules/tests/evaluation.rs +++ b/src/engine/modules/tests/evaluation.rs @@ -20,7 +20,7 @@ fn dependency_free_module_links_then_evaluates_with_module_semantics() { let snapshot = module_evaluation_snapshot(&mut context, &module); assert_eq!(snapshot.state, PromiseState::Fulfilled); - assert_eq!(snapshot.result, RawValue::Undefined); + assert!(matches!(snapshot.result, RawValue::Undefined)); assert_script_true( &mut context, r#" @@ -51,7 +51,7 @@ fn module_identity_evaluates_once_and_caches_abrupt_completion() { let first = module_evaluation_promise(&mut context, &abrupt); let first_snapshot = promise_snapshot(&runtime, &first); assert_eq!(first_snapshot.state, PromiseState::Rejected); - assert_eq!(first_snapshot.result, RawValue::Int(42)); + assert!(matches!(first_snapshot.result, RawValue::Int(42))); let second = module_evaluation_promise(&mut context, &abrupt); assert_eq!(first.object_id(), second.object_id()); } diff --git a/src/engine/modules/tests/graph_evaluation.rs b/src/engine/modules/tests/graph_evaluation.rs index f3b38209..ed359b09 100644 --- a/src/engine/modules/tests/graph_evaluation.rs +++ b/src/engine/modules/tests/graph_evaluation.rs @@ -80,7 +80,7 @@ fn dependency_evaluation_exception_is_cached_on_every_active_ancestor() { let first = module_evaluation_promise(&mut context, &module); let first_snapshot = promise_snapshot(&runtime, &first); assert_eq!(first_snapshot.state, PromiseState::Rejected); - assert_eq!(first_snapshot.result, RawValue::Int(42)); + assert!(matches!(first_snapshot.result, RawValue::Int(42))); let second = module_evaluation_promise(&mut context, &module); assert_eq!(first.object_id(), second.object_id()); assert_script_true( @@ -123,7 +123,7 @@ fn cyclic_evaluation_exception_is_cached_on_the_complete_active_scc() { let first = module_evaluation_promise(&mut context, &module); let first_snapshot = promise_snapshot(&runtime, &first); assert_eq!(first_snapshot.state, PromiseState::Rejected); - assert_eq!(first_snapshot.result, RawValue::Int(42)); + assert!(matches!(first_snapshot.result, RawValue::Int(42))); let second = module_evaluation_promise(&mut context, &module); assert_eq!(first.object_id(), second.object_id()); for _ in 0..2 { diff --git a/src/engine/modules/tests/ownership.rs b/src/engine/modules/tests/ownership.rs index 57fd64a8..146048ed 100644 --- a/src/engine/modules/tests/ownership.rs +++ b/src/engine/modules/tests/ownership.rs @@ -82,7 +82,7 @@ fn cloned_module_handle_roots_compilation_and_first_link_realms() { assert_eq!(runtime.heap_counts().context_nodes, 2); let snapshot = module_evaluation_snapshot(&mut link_context, &surviving_handle); assert_eq!(snapshot.state, PromiseState::Fulfilled); - assert_eq!(snapshot.result, RawValue::Undefined); + assert!(matches!(snapshot.result, RawValue::Undefined)); assert_script_true(&mut link_context, "__rootedModuleRealm === 42"); } @@ -109,14 +109,14 @@ fn cross_linked_module_caches_do_not_leak_a_context_cycle() { second_context.execute_module(&first_module).unwrap(); first_context.execute_module(&second_module).unwrap(); - assert_eq!( + assert!(matches!( runtime.module_record(first_module.raw).unwrap().link_realm, - Some(RawModuleLinkRealm::Other(second_context.realm)) - ); - assert_eq!( + Some(RawModuleLinkRealm::Other(realm)) if realm == second_context.realm + )); + assert!(matches!( runtime.module_record(second_module.raw).unwrap().link_realm, - Some(RawModuleLinkRealm::Other(first_context.realm)) - ); + Some(RawModuleLinkRealm::Other(realm)) if realm == first_context.realm + )); assert_eq!(runtime.heap_counts().context_nodes, 2); drop(first_module); diff --git a/src/engine/modules/tests/top_level_await.rs b/src/engine/modules/tests/top_level_await.rs index 8d66c41a..17cee5c6 100644 --- a/src/engine/modules/tests/top_level_await.rs +++ b/src/engine/modules/tests/top_level_await.rs @@ -347,7 +347,7 @@ fn shared_tla_completion_executes_cross_linked_parents_in_callback_realm() { ); let parent_snapshot = promise_snapshot(&runtime, &parent_promise); assert_eq!(parent_snapshot.state, PromiseState::Rejected); - assert_eq!(parent_snapshot.result, RawValue::Int(42)); + assert!(matches!(parent_snapshot.result, RawValue::Int(42))); assert_eq!( promise_snapshot(&runtime, &async_parent_promise).state, PromiseState::Fulfilled diff --git a/src/engine/object/access.rs b/src/engine/object/access.rs index a778a591..d8974ac8 100644 --- a/src/engine/object/access.rs +++ b/src/engine/object/access.rs @@ -1,7 +1,7 @@ use crate::engine::api::error::{ErrorKind, NativeErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::builtins::native::PrimitiveKind; use crate::engine::heap::runtime::RuntimeState; @@ -10,7 +10,7 @@ use crate::engine::object::operations::RawStringProperty; use crate::engine::object::ordinary::OrdinaryRead; use crate::engine::object::{ObjectRef, PropertyKey}; use crate::engine::value::conversion::NativeConversion; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::Completion; impl Runtime { @@ -56,11 +56,11 @@ impl Runtime { strict: bool, ) -> Result { match result { - NativeConversion::Throw(value) => Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => Ok(Completion::Throw(self.unroot_value(&value)?)), NativeConversion::Value(false) if strict => Err(RuntimeError::Engine( crate::engine::api::Error::new(ErrorKind::Type, "could not delete property"), )), - NativeConversion::Value(value) => Ok(Completion::Return(Value::Bool(value))), + NativeConversion::Value(value) => Ok(Completion::Return(JsValue::Bool(value))), } } @@ -86,15 +86,23 @@ impl Runtime { && let Ok(index) = usize::try_from(index) && let Some(unit) = string.code_unit_at(index) { - return Ok(OrdinaryRead::Complete(Some(Value::String( - JsString::from_code_unit(unit), - )))); + // A fresh string payload is a genuine creation point: publish it + // as one owned arena node. + let id = self + .0 + .state + .borrow_mut() + .heap + .allocate_string(JsString::from_code_unit(unit))?; + return Ok(OrdinaryRead::Complete(Some( + crate::engine::value::JsValue::String(id), + ))); } let length = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?; if key == &length { let length = i32::try_from(string.len()) - .map(Value::Int) - .unwrap_or_else(|_| Value::number(string.len() as f64)); + .map(crate::engine::value::JsValue::Int) + .unwrap_or_else(|_| crate::engine::value::JsValue::Float(string.len() as f64)); return Ok(OrdinaryRead::Complete(Some(length))); } let prototype = self.primitive_prototype_for_realm(realm, PrimitiveKind::String)?; @@ -120,6 +128,28 @@ impl Runtime { ) -> Result { self.prepare_value_property_read_selected(realm, receiver, key, None) } + /// Internal-value receiver form: the slow-path read roots the receiver + /// once for accessor/prototype Call selection. + pub(crate) fn prepare_value_property_read_selected_jsvalue( + &self, + realm: ContextId, + receiver: &crate::engine::value::JsValue, + key: &PropertyKey, + native: Option<&mut Option>, + ) -> Result { + let receiver_root = self.root_value(receiver)?; + self.prepare_value_property_read_selected(realm, &receiver_root, key, native) + } + + pub(crate) fn prepare_value_property_read_borrowed_jsvalue( + &self, + realm: ContextId, + receiver: &crate::engine::value::JsValue, + key: &PropertyKey, + ) -> Result { + self.prepare_value_property_read_selected_jsvalue(realm, receiver, key, None) + } + pub(crate) fn prepare_value_property_read_selected( &self, realm: ContextId, @@ -173,8 +203,11 @@ impl Runtime { read: OrdinaryRead, ) -> Result { Ok(match self.finish_prepared_read(realm, key, read)? { - NativeConversion::Value(value) => Completion::Return(value.unwrap_or(Value::Undefined)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => Completion::Return(match value { + Some(value) => self.unroot_value(&value)?, + None => JsValue::Undefined, + }), + NativeConversion::Throw(value) => Completion::Throw(self.unroot_value(&value)?), }) } @@ -207,7 +240,7 @@ impl Runtime { ) -> Result { match self.prepare_value_property_read_completion(realm, receiver, key)? { NativeConversion::Value(read) => self.finish_value_property_read(realm, key, read), - NativeConversion::Throw(reason) => Ok(Completion::Throw(reason)), + NativeConversion::Throw(reason) => Ok(Completion::Throw(self.unroot_value(&reason)?)), } } @@ -235,7 +268,7 @@ pub(crate) fn raw_string_property_on_object( ) -> Result { let object = state.heap.object(object)?; let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(atom) else { + let Some(index) = shape.find(AtomIdx::from_raw(atom.raw())) else { return Ok(RawStringProperty::Missing); }; let slot = object @@ -245,10 +278,14 @@ pub(crate) fn raw_string_property_on_object( "backtrace name shape has no parallel property slot", ))?; Ok(match slot { - PropertySlot::Data(RawValue::String(value)) if value.is_flat() => { - RawStringProperty::String(value.clone()) + PropertySlot::Data(RawValue::String(value)) => { + let string = state.heap.string(*value)?; + if string.is_flat() { + RawStringProperty::String(string.clone()) + } else { + RawStringProperty::Other + } } - PropertySlot::Data(RawValue::String(_)) => RawStringProperty::Other, PropertySlot::Data(_) | PropertySlot::VarRef(_) | PropertySlot::Accessor { .. } diff --git a/src/engine/object/allocation.rs b/src/engine/object/allocation.rs index 6662adaf..d28ae13c 100644 --- a/src/engine/object/allocation.rs +++ b/src/engine/object/allocation.rs @@ -1,6 +1,6 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::builtins::native::{NativeFunctionId, PrimitiveKind}; use crate::engine::code::function::metadata::{ @@ -73,7 +73,7 @@ impl Runtime { } let length = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?; let entries = [ShapeEntry { - atom: length.atom(), + atom: AtomIdx::from_raw(length.atom().raw()), flags: PropertyFlags::data(true, false, false), }]; let mut state = self.0.state.borrow_mut(); @@ -117,15 +117,32 @@ impl Runtime { } self.validate_value_domain(&value, "Array element")?; let raw = self.raw_property_value(&value)?; + // Clone duplicates only the handle; the probe keeps the producer edge + // accountable through every store-or-decline path below. + let conversion_probe = raw.clone(); let mut state = self.0.state.borrow_mut(); - let retained_atoms = state.retain_raw_value_atoms(std::iter::once(&raw))?; - match state + let retained_atoms = match state.retain_raw_value_atoms(std::iter::once(&raw)) { + Ok(atoms) => atoms, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } + }; + let appended = state .heap - .append_fresh_array_dense_value(array.object_id(), raw) - { - Ok(()) => Ok(()), + .append_fresh_array_dense_value(array.object_id(), raw); + match appended { + Ok(()) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + Ok(()) + } Err(error) => { - state.release_atoms(retained_atoms)?; + let released = state.release_atoms(retained_atoms); + drop(state); + self.release_converted_value_edge(&conversion_probe); + released?; Err(error.into()) } } @@ -149,6 +166,53 @@ impl Runtime { Ok(array) } + /// Internal-value form of [`Runtime::new_array_from_values`]: consumes the + /// values' edges after each dense store has retained its own copy. + pub(crate) fn new_array_from_values_jsvalue( + &self, + realm: ContextId, + values: Vec, + ) -> Result { + let array = self.new_array(realm)?; + for value in values { + self.append_fresh_array_value_jsvalue(&array, value)?; + } + Ok(array) + } + + /// Internal-value form of [`Runtime::append_fresh_array_value`]. The dense + /// store retains its own copy edge transactionally; the consumed value's + /// edge is released before returning. + pub(crate) fn append_fresh_array_value_jsvalue( + &self, + array: &ObjectRef, + value: crate::engine::value::JsValue, + ) -> Result<(), RuntimeError> { + if !array.belongs_to(self) { + return Err(RuntimeError::WrongRuntime("Array")); + } + let raw = value.as_raw(); + let mut state = self.0.state.borrow_mut(); + let retained_atoms = state.retain_raw_value_atoms(std::iter::once(&raw))?; + let appended = state + .heap + .append_fresh_array_dense_value(array.object_id(), raw); + match appended { + Ok(()) => { + drop(state); + self.release_jsvalue(value)?; + Ok(()) + } + Err(error) => { + let released = state.release_atoms(retained_atoms); + drop(state); + self.release_jsvalue(value)?; + released?; + Err(error.into()) + } + } + } + pub(crate) fn new_string_iterator( &self, realm: ContextId, @@ -230,6 +294,24 @@ impl Runtime { self.new_primitive_object_with_string_length(prototype, kind, value, false) } + /// Internal-value form of [`Runtime::new_primitive_object`]: consumes the + /// wrapper payload's edges after the wrapper has retained its own copies. + pub(crate) fn new_primitive_object_jsvalue( + &self, + prototype: &ObjectRef, + kind: PrimitiveKind, + value: crate::engine::value::JsValue, + ) -> Result { + let object = self.new_primitive_object_with_string_length( + prototype, + kind, + self.root_value(&value)?, + false, + )?; + self.release_jsvalue(value)?; + Ok(object) + } + pub(crate) fn new_string_object( &self, prototype: &ObjectRef, @@ -470,22 +552,72 @@ impl Runtime { } let raw_this = self.raw_property_value(this_value)?; - let raw_arguments = arguments - .iter() - .map(|argument| self.raw_property_value(argument)) - .collect::, _>>()?; - let is_constructor = self.is_constructor(target.as_object())?; + let mut raw_arguments = Vec::with_capacity(arguments.len()); + for argument in arguments { + match self.raw_property_value(argument) { + Ok(raw) => raw_arguments.push(raw), + Err(error) => { + // Nothing was stored yet; balance every producer edge. + self.release_converted_value_edge(&raw_this); + for raw in &raw_arguments { + self.release_converted_value_edge(raw); + } + return Err(error); + } + } + } + // Clones duplicate only the handles; the probes keep every producer + // edge accountable through every store-or-decline path below. + let conversion_probes: Vec<_> = std::iter::once(raw_this.clone()) + .chain(raw_arguments.iter().cloned()) + .collect(); + let is_constructor = match self.is_constructor(target.as_object()) { + Ok(is_constructor) => is_constructor, + Err(error) => { + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } + return Err(error); + } + }; let mut state = self.0.state.borrow_mut(); - let function_prototype = state.heap.context(realm)?.function_prototype; - let shape = state.get_or_create_shape(Some(function_prototype), &[])?; + let shape = { + let created = match state + .heap + .context(realm) + .map(|context| context.function_prototype) + .map_err(RuntimeError::from) + { + Ok(prototype) => state.get_or_create_shape(Some(prototype), &[]), + Err(error) => Err(error), + }; + match created { + Ok(shape) => shape, + Err(error) => { + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } + return Err(error); + } + } + }; let retained_atoms = match state .retain_raw_value_atoms(std::iter::once(&raw_this).chain(raw_arguments.iter())) { Ok(atoms) => atoms, Err(error) => { - let cleanup = state.heap.release_shape(shape)?; - state.apply_cleanup(cleanup)?; + let applied = state + .heap + .release_shape(shape) + .map_err(RuntimeError::from) + .and_then(|cleanup| state.apply_cleanup(cleanup)); + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } + applied?; return Err(error); } }; @@ -499,15 +631,33 @@ impl Runtime { )) { Ok(object) => object, Err(error) => { - state.release_atoms(retained_atoms)?; - let cleanup = state.heap.release_shape(shape)?; - state.apply_cleanup(cleanup)?; + let released = state.release_atoms(retained_atoms); + let applied = state + .heap + .release_shape(shape) + .map_err(RuntimeError::from) + .and_then(|cleanup| state.apply_cleanup(cleanup)); + drop(state); + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } + released?; + applied?; return Err(error.into()); } }; - let cleanup = state.heap.release_shape(shape)?; - state.apply_cleanup(cleanup)?; + let finalized = state + .heap + .release_shape(shape) + .map_err(RuntimeError::from) + .and_then(|cleanup| state.apply_cleanup(cleanup)); drop(state); + // The bound function retained its own copy edges; the boundary + // conversions' producer edges are no longer needed. + for probe in &conversion_probes { + self.release_converted_value_edge(probe); + } + finalized?; Ok(CallableRef::from_validated_object( ObjectRef::from_owned_handle(self.clone(), object), )) @@ -601,11 +751,37 @@ impl Runtime { /// Returns `None` for objects without `[[Call]]`; runtime-domain and stale /// handle failures remain explicit errors. pub fn as_callable(&self, object: &ObjectRef) -> Result, RuntimeError> { + self.as_callable_object(object.object_id()) + } + + /// Handle form of [`Runtime::as_callable`]; borrows the object's edge. + pub(crate) fn as_callable_object( + &self, + object: crate::engine::heap::ObjectId, + ) -> Result, RuntimeError> { let _operation = self.operation(); - if !self.object_has_call_capability(object)? { + if !self.object_id_has_call_capability(object)? { return Ok(None); } - Ok(Some(CallableRef::from_validated_object(object.clone()))) + Ok(Some(CallableRef::from_validated_object( + ObjectRef::from_borrowed_handle(self.clone(), object)?, + ))) + } + + fn object_id_has_call_capability( + &self, + object: crate::engine::heap::ObjectId, + ) -> Result { + Ok(matches!( + self.0.state.borrow().heap.object(object)?.payload, + crate::engine::heap::ObjectPayload::NativeFunction { .. } + | crate::engine::heap::ObjectPayload::BoundFunction { .. } + | crate::engine::heap::ObjectPayload::BytecodeFunction { .. } + | crate::engine::heap::ObjectPayload::Proxy(crate::engine::heap::ProxyData { + is_callable: true, + .. + }) + )) } /// The inner error returns the unchanged non-callable owner so callers can diff --git a/src/engine/object/arguments.rs b/src/engine/object/arguments.rs index e89f2782..b7899a12 100644 --- a/src/engine/object/arguments.rs +++ b/src/engine/object/arguments.rs @@ -7,6 +7,7 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; use crate::engine::heap::roots::VarRefRoot; use crate::engine::heap::{ContextId, ObjectData, ObjectPayload, PropertySlot, RawValue}; @@ -42,7 +43,7 @@ impl ArgumentsLayout { fn push(&mut self, key: PropertyKey, flags: PropertyFlags, slot: PropertySlot) { self.entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags, }); self.slots.push(slot); @@ -65,16 +66,62 @@ impl Runtime { RuntimeError::Invariant("actual argument count exceeded QuickJS Uint32 storage") })?; let mut layout = ArgumentsLayout::new(values.len()); - for (index, value) in values.iter().enumerate() { - let key = self.property_key_for_index(index as u64)?; + // Convert up front: node allocation needs the state borrow, while the + // layout commit happens inside `new_arguments_object_base`. Clone + // copies only the handle; each conversion keeps exactly one producer + // edge, released once below (or immediately on every failure exit). + let mut converted = Vec::with_capacity(values.len()); + for value in &values { + if let Err(error) = self + .raw_property_value(value) + .map(|raw| converted.push(raw)) + { + for raw in &converted { + self.release_converted_value_edge(raw); + } + return Err(error); + } + } + for (index, raw) in converted.iter().enumerate() { + let key = match self.property_key_for_index(index as u64) { + Ok(key) => key, + Err(error) => { + for raw in &converted { + self.release_converted_value_edge(raw); + } + return Err(RuntimeError::from(error)); + } + }; layout.push( key, PropertyFlags::data(true, true, true), - PropertySlot::Data(self.raw_property_value(value)?), + PropertySlot::Data(raw.clone()), ); } - self.prepare_arguments_common_properties(realm, length, None, &mut layout)?; - self.new_arguments_object_base(realm, false, length, layout) + if let Err(error) = + self.prepare_arguments_common_properties(realm, length, None, &mut layout) + { + for raw in &converted { + self.release_converted_value_edge(raw); + } + return Err(error); + } + match self.new_arguments_object_base(realm, false, length, layout) { + Ok(object) => { + // The object retained its own copy edges during allocation; + // the boundary conversions' producer edges are no longer needed. + for raw in &converted { + self.release_converted_value_edge(raw); + } + Ok(object) + } + Err(error) => { + for raw in &converted { + self.release_converted_value_edge(raw); + } + Err(error) + } + } } /// Build QuickJS `JS_CLASS_MAPPED_ARGUMENTS`. Each supplied root is one @@ -304,7 +351,7 @@ impl Runtime { enumerable, configurable, } => { - self.write_var_ref(&var_ref, value)?; + self.write_var_ref(&var_ref, self.unroot_value(&value)?)?; self.store_property_slot( object, key, @@ -318,7 +365,7 @@ impl Runtime { let CompleteOrdinaryPropertyDescriptor::Data { value, .. } = &complete else { unreachable!() }; - self.write_var_ref(&var_ref, value.clone())?; + self.write_var_ref(&var_ref, self.unroot_value(value)?)?; self.store_complete_property(object, key, complete)?; } complete @ CompleteOrdinaryPropertyDescriptor::Accessor { .. } => { @@ -343,7 +390,7 @@ impl Runtime { let state = self.0.state.borrow(); let object_data = state.heap.object(object.object_id())?; let shape = state.heap.shape(object_data.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(false); }; let index = usize::try_from(index) @@ -359,15 +406,16 @@ impl Runtime { match slot { PropertySlot::VarRef(id) => { let root = VarRefRoot::from_borrowed_handle(self.clone(), id)?; - self.write_var_ref(&root, value.clone())?; + self.write_var_ref(&root, self.unroot_value(value)?)?; } PropertySlot::Data(_) => { - self.store_property_slot( - object, - key, - flags, - PropertySlot::Data(self.raw_property_value(value)?), - )?; + let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through the store below. + let conversion_probe = raw.clone(); + let stored = self.store_property_slot(object, key, flags, PropertySlot::Data(raw)); + self.release_converted_value_edge(&conversion_probe); + stored?; } PropertySlot::Accessor { .. } | PropertySlot::AutoInit(_) => return Ok(false), } @@ -379,6 +427,7 @@ impl Runtime { mod tests { use crate::engine::code::function::metadata::ClosureVariableKind; use crate::engine::object::DescriptorField; + use crate::engine::value::JsValue; use super::*; @@ -544,7 +593,7 @@ mod tests { let mut context = runtime.new_context(); let callee = context.function_prototype().unwrap(); let root = runtime - .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .new_var_ref(JsValue::Int(1), false, false, ClosureVariableKind::Normal) .unwrap(); let arguments = runtime .new_mapped_arguments_object(context.realm, &callee, vec![root.clone()]) @@ -556,10 +605,10 @@ mod tests { .set_property(&arguments, &zero, Value::Int(2)) .unwrap() ); - assert_eq!(runtime.read_var_ref(&root).unwrap(), Value::Int(2)); + assert_eq!(runtime.read_var_ref(&root).unwrap(), JsValue::Int(2)); assert_eq!(runtime.arguments_fast_len(&arguments), Ok(Some(1))); - runtime.write_var_ref(&root, Value::Int(3)).unwrap(); + runtime.write_var_ref(&root, JsValue::Int(3)).unwrap(); assert_eq!( context.get_property(&arguments, &zero).unwrap(), Value::Int(3) @@ -577,9 +626,9 @@ mod tests { ) .unwrap() ); - assert_eq!(runtime.read_var_ref(&root).unwrap(), Value::Int(4)); + assert_eq!(runtime.read_var_ref(&root).unwrap(), JsValue::Int(4)); assert_eq!(runtime.arguments_fast_len(&arguments), Ok(None)); - runtime.write_var_ref(&root, Value::Int(5)).unwrap(); + runtime.write_var_ref(&root, JsValue::Int(5)).unwrap(); assert_eq!( context.get_property(&arguments, &zero).unwrap(), Value::Int(5) @@ -598,8 +647,8 @@ mod tests { ) .unwrap() ); - assert_eq!(runtime.read_var_ref(&root).unwrap(), Value::Int(6)); - runtime.write_var_ref(&root, Value::Int(7)).unwrap(); + assert_eq!(runtime.read_var_ref(&root).unwrap(), JsValue::Int(6)); + runtime.write_var_ref(&root, JsValue::Int(7)).unwrap(); assert_eq!( context.get_property(&arguments, &zero).unwrap(), Value::Int(6) @@ -622,10 +671,10 @@ mod tests { let mut context = runtime.new_context(); let callee = context.function_prototype().unwrap(); let first = runtime - .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .new_var_ref(JsValue::Int(1), false, false, ClosureVariableKind::Normal) .unwrap(); let second = runtime - .new_var_ref(Value::Int(2), false, false, ClosureVariableKind::Normal) + .new_var_ref(JsValue::Int(2), false, false, ClosureVariableKind::Normal) .unwrap(); let tail = runtime .new_mapped_arguments_object( @@ -649,7 +698,7 @@ mod tests { assert!(runtime.delete_property(&middle, &zero).unwrap()); assert_eq!(runtime.arguments_fast_len(&middle), Ok(None)); assert!(context.set_property(&middle, &zero, Value::Int(8)).unwrap()); - runtime.write_var_ref(&first, Value::Int(9)).unwrap(); + runtime.write_var_ref(&first, JsValue::Int(9)).unwrap(); assert_eq!(context.get_property(&middle, &zero).unwrap(), Value::Int(8)); } } diff --git a/src/engine/object/array_length.rs b/src/engine/object/array_length.rs index 91bec010..c2355688 100644 --- a/src/engine/object/array_length.rs +++ b/src/engine/object/array_length.rs @@ -2,12 +2,12 @@ use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::heap::ContextId; use crate::engine::object::operations::ArrayLengthConversion; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; pub(crate) enum ArrayLengthStep { Complete(ArrayLengthConversion), Number { - value: Value, + value: JsValue, resume: ArrayLengthResume, }, } @@ -49,7 +49,7 @@ impl ArrayLengthStep { } Value::Int(_) => Self::Complete(runtime.invalid_array_length(realm)?), value => Self::Number { - value: value.clone(), + value: runtime.unroot_value(&value)?, resume: ArrayLengthResume(Box::new(ArrayLengthResumeState { realm, phase: Phase::First(value), @@ -74,7 +74,7 @@ impl ArrayLengthResume { }; Ok(match self.0.phase { Phase::First(original) => ArrayLengthStep::Number { - value: original.clone(), + value: runtime.unroot_value(&original)?, resume: Self(Box::new(ArrayLengthResumeState { realm: self.0.realm, phase: Phase::Second { @@ -97,10 +97,11 @@ const _: () = assert!(std::mem::size_of::() <= 64); mod tests { use super::*; - fn take_number(step: ArrayLengthStep) -> ArrayLengthResume { - let ArrayLengthStep::Number { resume, .. } = step else { + fn take_number(runtime: &Runtime, step: ArrayLengthStep) -> ArrayLengthResume { + let ArrayLengthStep::Number { value, resume } = step else { panic!("expected ToNumber request") }; + runtime.release_jsvalue(value).unwrap(); resume } @@ -113,11 +114,13 @@ mod tests { let original = runtime.new_object(None).unwrap(); let id = original.object_id(); let mut resume = take_number( + &runtime, ArrayLengthStep::start(&runtime, Some(context.realm), Value::Object(original)) .unwrap(), ); if second { resume = take_number( + &runtime, resume .number(&runtime, NativeConversion::Value(1.0)) .unwrap(), diff --git a/src/engine/object/array_storage.rs b/src/engine/object/array_storage.rs index 84c62743..71983fce 100644 --- a/src/engine/object/array_storage.rs +++ b/src/engine/object/array_storage.rs @@ -38,7 +38,7 @@ impl Runtime { let indices = shape .entries() .iter() - .map(|entry| state.atoms.array_index(entry.atom)) + .map(|entry| state.atoms.array_index(state.atoms.brand(entry.atom)?)) .collect::, _>>()?; // Descending deletion stops at the highest non-configurable index. // Everything above it is removed; nothing at or below it is touched. diff --git a/src/engine/object/builtin_properties.rs b/src/engine/object/builtin_properties.rs index b13f09d9..17d92d29 100644 --- a/src/engine/object/builtin_properties.rs +++ b/src/engine/object/builtin_properties.rs @@ -4,6 +4,7 @@ use super::ObjectRef; use super::shape::{PropertyFlags, PropertyStorageKind, ShapeEntry}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeFunctionId; use crate::engine::heap::{AutoInitProperty, ContextId, HeapError, ObjectPayload, PropertySlot}; use std::collections::HashSet; @@ -84,7 +85,7 @@ impl Runtime { for (key, method) in &properties { if method.flags.storage != PropertyStorageKind::Data || state.atoms.array_index(key.atom())?.is_some() - || shape.find(key.atom()).is_some() + || shape.find(AtomIdx::from_raw(key.atom().raw())).is_some() || !seen.insert(key.atom()) { return Err(RuntimeError::Invariant( @@ -110,7 +111,7 @@ impl Runtime { })?; for (key, method) in &properties { entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: method.flags, }); slots.push(PropertySlot::AutoInit(AutoInitProperty::NativeBuiltin { diff --git a/src/engine/object/class.rs b/src/engine/object/class.rs index f4de5cb9..e22c2c7d 100644 --- a/src/engine/object/class.rs +++ b/src/engine/object/class.rs @@ -56,7 +56,10 @@ impl Runtime { &parent_constructor, &prototype_key, )? { - Completion::Return(value) => Self::class_parent_prototype(value)?, + Completion::Return(value) => { + let value = self.root_and_release_jsvalue(value)?; + Self::class_parent_prototype(value)? + } Completion::Throw(value) => { return Ok(DefineClassOutcome::Throw(value)); } @@ -191,8 +194,8 @@ impl Runtime { )?; Ok(DefineClassOutcome::Defined { - constructor: Value::Object(constructor.as_object().clone()), - prototype: Value::Object(prototype), + constructor: self.unroot_value(&Value::Object(constructor.as_object().clone()))?, + prototype: self.unroot_value(&Value::Object(prototype))?, }) } @@ -375,10 +378,15 @@ mod tests { panic!("base class definition unexpectedly threw") }; assert_eq!( - returned_constructor, + runtime + .root_and_release_jsvalue(returned_constructor) + .unwrap(), Value::Object(constructor.as_object().clone()) ); - let Value::Object(prototype) = returned_prototype else { + let Value::Object(prototype) = runtime + .root_and_release_jsvalue(returned_prototype) + .unwrap() + else { panic!("class prototype was not an object") }; diff --git a/src/engine/object/dense_mutation.rs b/src/engine/object/dense_mutation.rs index 059f10ce..62b6681c 100644 --- a/src/engine/object/dense_mutation.rs +++ b/src/engine/object/dense_mutation.rs @@ -46,28 +46,66 @@ impl Runtime { } self.validate_value_domain(value, "property value")?; let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); let mut state = self.0.state.borrow_mut(); let id = object.object_id(); - let data = state.heap.object(id)?; - let Some(length) = writable_dense_length(&state, data)? else { - return Ok(None); - }; - if !data.extensible { - return Ok(None); - } - let Some(atom) = Atom::from_immediate_integer(length) else { - return Ok(None); + let prepared = (|| -> Result, RuntimeError> { + let data = state.heap.object(id)?; + let Some(length) = writable_dense_length(&state, data)? else { + return Ok(None); + }; + if !data.extensible { + return Ok(None); + } + let Some(atom) = Atom::from_immediate_integer(length) else { + return Ok(None); + }; + let prototype = state.heap.shape(data.shape)?.prototype(); + if !prototypes_allow_dense_append(&state, atom, prototype)? { + return Ok(None); + } + Ok(Some(length)) + })(); + // Every decline leaves the dense storage untouched; the value's + // producer edge must be balanced before returning. + let length = match prepared { + Ok(Some(length)) => length, + Ok(None) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Ok(None); + } + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } }; - let prototype = state.heap.shape(data.shape)?.prototype(); - if !prototypes_allow_dense_append(&state, atom, prototype)? { - return Ok(None); - } // The shared allocation/edge kernel commits the element before length. // The explicit final Set(length) is a no-op on this writable own slot. - let retained = state.retain_raw_value_atoms(std::iter::once(&raw))?; - if let Err(error) = state.heap.append_fresh_array_dense_value(id, raw) { - state.release_atoms(retained)?; - return Err(error.into()); + let retained = match state.retain_raw_value_atoms(std::iter::once(&raw)) { + Ok(retained) => retained, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } + }; + let appended = state.heap.append_fresh_array_dense_value(id, raw); + match appended { + Ok(()) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + } + Err(error) => { + let released = state.release_atoms(retained); + drop(state); + self.release_converted_value_edge(&conversion_probe); + released?; + return Err(error.into()); + } } #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event("array_mutation_dense_push"); diff --git a/src/engine/object/dictionary.rs b/src/engine/object/dictionary.rs index 0bf7996e..d7b1309d 100644 --- a/src/engine/object/dictionary.rs +++ b/src/engine/object/dictionary.rs @@ -56,7 +56,10 @@ impl RuntimeState { let mut incoming = HashMap::with_capacity(entries.len()); for (index, entry) in entries.iter().enumerate() { if incoming.insert(entry.atom, index).is_some() { - return Err(super::shape::ShapeError::DuplicateAtom(entry.atom).into()); + return Err(super::shape::ShapeError::DuplicateAtom( + crate::engine::atom::Atom::from_raw(entry.atom.raw()), + ) + .into()); } } let mut pairs = entries diff --git a/src/engine/object/function_initialization.rs b/src/engine/object/function_initialization.rs index 4f7a7da0..2b997026 100644 --- a/src/engine/object/function_initialization.rs +++ b/src/engine/object/function_initialization.rs @@ -1,6 +1,7 @@ use crate::engine::api::error::{Error, ErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; use crate::engine::builtins::native::NativeFunctionId; use crate::engine::code::function::metadata::{ConstructorKind, FunctionKind, FunctionMetadata}; @@ -200,7 +201,7 @@ impl Runtime { let (prototype, mut entries, mut slots) = { let object = state.heap.object(object_id)?; let shape = state.heap.shape(object.shape)?; - if shape.find(key.atom()).is_some() { + if shape.find(AtomIdx::from_raw(key.atom().raw())).is_some() { return Err(RuntimeError::Invariant( "function prototype autoinit property already exists", )); @@ -212,7 +213,7 @@ impl Runtime { ) }; entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: PropertyFlags::data(true, false, false), }); slots.push(PropertySlot::AutoInit( @@ -262,7 +263,7 @@ impl Runtime { let (prototype, mut entries, mut slots) = { let object = state.heap.object(object_id)?; let shape = state.heap.shape(object.shape)?; - if shape.find(key.atom()).is_some() { + if shape.find(AtomIdx::from_raw(key.atom().raw())).is_some() { return Err(RuntimeError::Invariant( "native builtin autoinit property already exists", )); @@ -274,7 +275,7 @@ impl Runtime { ) }; entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags, }); slots.push(PropertySlot::AutoInit(AutoInitProperty::NativeBuiltin { @@ -301,7 +302,7 @@ impl Runtime { let (prototype, mut entries, mut slots) = { let object = state.heap.object(object_id)?; let shape = state.heap.shape(object.shape)?; - if shape.find(key.atom()).is_some() { + if shape.find(AtomIdx::from_raw(key.atom().raw())).is_some() { return Err(RuntimeError::Invariant( "string autoinit property already exists", )); @@ -313,7 +314,7 @@ impl Runtime { ) }; entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: PropertyFlags::data(true, false, true), }); slots.push(PropertySlot::AutoInit(AutoInitProperty::String { @@ -344,7 +345,7 @@ impl Runtime { ) }; entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: PropertyFlags::data(true, false, true), }); slots.push(PropertySlot::AutoInit(AutoInitProperty::FailureProbe { diff --git a/src/engine/object/internal_methods.rs b/src/engine/object/internal_methods.rs index 50d109ee..73760737 100644 --- a/src/engine/object/internal_methods.rs +++ b/src/engine/object/internal_methods.rs @@ -24,8 +24,8 @@ use crate::engine::object::{ AccessorValue, CallableRef, CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, SymbolRef, }; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{ConstructNewTarget, ConstructorRef, DirectCallTarget}; @@ -147,6 +147,35 @@ impl Runtime { "not a function", ))); }; + self.direct_call_target_from_object(object) + } + + /// Handle form of [`Runtime::direct_call_target_from_value`]; borrows the + /// object's edge and roots the classified capability only. + pub(crate) fn direct_call_target_from_jsvalue( + &self, + value: crate::engine::value::JsValue, + ) -> Result { + let object = match value { + crate::engine::value::JsValue::Object(object) => object, + other => { + self.release_jsvalue(other)?; + return Err(RuntimeError::Engine(Error::new( + ErrorKind::Type, + "not a function", + ))); + } + }; + self.direct_call_target_from_object(crate::engine::object::ObjectRef::from_owned_handle( + self.clone(), + object, + )) + } + + fn direct_call_target_from_object( + &self, + object: crate::engine::object::ObjectRef, + ) -> Result { if !object.belongs_to(self) { return Err(RuntimeError::WrongRuntime("call target")); } @@ -559,11 +588,15 @@ impl Runtime { realm, ProxyPrototypeStep::start(self, realm, object.clone(), ProxyPrototypeKind::Get)?, )? { - Completion::Return(Value::Object(object)) => Ok(NativeConversion::Value(Some(object))), - Completion::Return(Value::Null) => Ok(NativeConversion::Value(None)), - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), - _ => Err(RuntimeError::Invariant( - "GetPrototypeOf completed with an invalid value", + Completion::Return(value) => match self.root_and_release_jsvalue(value)? { + Value::Object(object) => Ok(NativeConversion::Value(Some(object))), + Value::Null => Ok(NativeConversion::Value(None)), + _ => Err(RuntimeError::Invariant( + "GetPrototypeOf completed with an invalid value", + )), + }, + Completion::Throw(value) => Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, )), } } @@ -589,10 +622,14 @@ impl Runtime { ProxyPrototypeKind::Set(prototype.cloned()), )?, )? { - Completion::Return(Value::Bool(value)) => Ok(NativeConversion::Value(value)), - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), - _ => Err(RuntimeError::Invariant( - "SetPrototypeOf completed with an invalid value", + Completion::Return(value) => match self.root_and_release_jsvalue(value)? { + Value::Bool(value) => Ok(NativeConversion::Value(value)), + _ => Err(RuntimeError::Invariant( + "SetPrototypeOf completed with an invalid value", + )), + }, + Completion::Throw(value) => Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, )), } } @@ -727,10 +764,11 @@ impl Runtime { ) -> Result { Ok( match self.internal_get_or_missing(realm, object, key, receiver)? { - NativeConversion::Value(value) => { - Completion::Return(value.unwrap_or(Value::Undefined)) - } - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => Completion::Return(match value { + Some(value) => self.unroot_value(&value)?, + None => JsValue::Undefined, + }), + NativeConversion::Throw(value) => Completion::Throw(self.unroot_value(&value)?), }, ) } @@ -773,8 +811,12 @@ impl Runtime { // Proxy Get observes undefined even when its target lookup is missing. // Non-Proxy descriptor/prototype work is shared by prepared reads. Ok(match self.proxy_get(realm, object, key, receiver)? { - Completion::Return(value) => NativeConversion::Value(Some(value)), - Completion::Throw(value) => NativeConversion::Throw(value), + Completion::Return(value) => { + NativeConversion::Value(Some(self.root_and_release_jsvalue(value)?)) + } + Completion::Throw(value) => { + NativeConversion::Throw(self.root_and_release_jsvalue(value)?) + } }) } @@ -792,13 +834,17 @@ impl Runtime { ProxyGetStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = self.root_and_release_jsvalue(resume.take_read_receiver())?; resume.resume(self, self.internal_get(realm, &object, &key, receiver)?)? } ProxyGetStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let completion = match target { DirectCallTarget::Callable(callable) => { @@ -856,7 +902,9 @@ impl Runtime { Completion::Return(_) => { Ok(NativeConversion::Value(InternalSetResult::Accepted)) } - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), + Completion::Throw(value) => Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )), } } } @@ -998,13 +1046,17 @@ impl Runtime { ProxySetStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = self.root_and_release_jsvalue(resume.take_read_receiver())?; resume.resume(self, self.internal_get(realm, &object, &key, receiver)?)? } ProxySetStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let completion = match target { DirectCallTarget::Callable(callable) => { @@ -1020,8 +1072,8 @@ impl Runtime { ProxySetStep::Set { mut resume } => { let object = resume.take_set_object(); let key = resume.take_set_key(); - let value = resume.take_set_value(); - let receiver = resume.take_set_receiver(); + let value = self.root_and_release_jsvalue(resume.take_set_value())?; + let receiver = self.root_and_release_jsvalue(resume.take_set_receiver())?; resume.set(self.internal_set(realm, &object, &key, value, receiver)?)? } ProxySetStep::Descriptor { mut resume } => { @@ -1110,13 +1162,17 @@ impl Runtime { ProxyOwnStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = self.root_and_release_jsvalue(resume.take_read_receiver())?; resume.resume(self, self.internal_get(realm, &object, &key, receiver)?)? } ProxyOwnStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let completion = match target { DirectCallTarget::Callable(callable) => { @@ -1140,7 +1196,7 @@ impl Runtime { resume.extensible(self.internal_is_extensible(realm, &object)?)? } ProxyOwnStep::Convert { mut resume } => { - let value = resume.take_convert_value(); + let value = self.root_and_release_jsvalue(resume.take_convert_value())?; resume.converted(self, self.native_to_property_descriptor(realm, value)?)? } }; @@ -1185,13 +1241,17 @@ impl Runtime { ProxyDefineStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = self.root_and_release_jsvalue(resume.take_read_receiver())?; resume.resume(self, self.internal_get(realm, &object, &key, receiver)?)? } ProxyDefineStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let completion = match target { DirectCallTarget::Callable(callable) => { @@ -1283,7 +1343,7 @@ impl Runtime { ProxyCallStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = self.root_and_release_jsvalue(resume.take_read_receiver())?; { let completion = self.internal_get(realm, &object, &key, receiver)?; resume.resume(self, completion)? @@ -1291,8 +1351,12 @@ impl Runtime { } ProxyCallStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| self.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let completion = match target { DirectCallTarget::Callable(callable) => { diff --git a/src/engine/object/internal_methods/boolean.rs b/src/engine/object/internal_methods/boolean.rs index ae49d097..02d04d6c 100644 --- a/src/engine/object/internal_methods/boolean.rs +++ b/src/engine/object/internal_methods/boolean.rs @@ -6,7 +6,7 @@ use super::{ use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::heap::ContextId; use crate::engine::object::{CompleteOrdinaryPropertyDescriptor, ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(crate) enum ProxyBooleanKind { @@ -107,7 +107,9 @@ fn method( step: MethodStep, ) -> Result { Ok(match step { - MethodStep::Throw(value) => ProxyBooleanStep::Complete(NativeConversion::Throw(value)), + MethodStep::Throw(value) => ProxyBooleanStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value.take())?, + )), MethodStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); @@ -117,7 +119,7 @@ fn method( key, receiver, ProxyBooleanResume(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm, phase: Phase::Method { resume, kind }, })), @@ -131,7 +133,7 @@ fn method( None => { let object = rooted.target.clone(); let resume = ProxyBooleanResume(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm, phase: Phase::Forward { _rooted: rooted, @@ -163,12 +165,17 @@ fn method( if let ProxyBooleanKind::Has(key) | ProxyBooleanKind::Delete(key) = &kind { arguments.push(runtime.property_key_value(key)?); } + let receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; ProxyBooleanStep::request_call( target, - Value::Object(rooted.handler.clone()), + receiver, arguments, ProxyBooleanResume(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm, phase: Phase::Trap { rooted, kind }, })), @@ -188,7 +195,9 @@ impl ProxyBooleanResume { let value = match completion { Completion::Return(value) => value, Completion::Throw(value) => { - return Ok(ProxyBooleanStep::Complete(NativeConversion::Throw(value))); + return Ok(ProxyBooleanStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } }; match self.0.phase { @@ -199,7 +208,7 @@ impl ProxyBooleanResume { resume.resume(runtime, Completion::Return(value))?, ), Phase::Trap { rooted, kind } => { - let result = runtime.value_to_boolean(&value)?; + let result = runtime.value_to_boolean_jsvalue(&value)?; match kind { ProxyBooleanKind::Has(_) if result => { Ok(ProxyBooleanStep::Complete(NativeConversion::Value(true))) @@ -208,7 +217,7 @@ impl ProxyBooleanResume { rooted.target.clone(), key.clone(), Self(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::HasInvariant { rooted, key }, })), @@ -222,7 +231,7 @@ impl ProxyBooleanResume { rooted.target.clone(), key.clone(), Self(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::DeleteInvariant { rooted, key }, })), @@ -231,7 +240,7 @@ impl ProxyBooleanResume { Ok(ProxyBooleanStep::request_extensible( rooted.target.clone(), Self(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::RequiredExtensibility { _rooted: rooted, @@ -245,7 +254,7 @@ impl ProxyBooleanResume { ProxyBooleanKind::Extensible => Ok(ProxyBooleanStep::request_extensible( rooted.target.clone(), Self(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::ExtensibleInvariant { _rooted: rooted, @@ -331,7 +340,7 @@ impl ProxyBooleanResume { return Ok(ProxyBooleanStep::request_extensible( rooted.target.clone(), Self(Box::new(ProxyBooleanResumeState { - pending_effect: ProxyBooleanStepPending::default(), + pending_effect: ProxyBooleanStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::RequiredExtensibility { _rooted: rooted, @@ -379,7 +388,7 @@ pub(super) fn finish( ProxyBooleanStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; resume.resume( runtime, runtime.internal_get(realm, &object, &key, receiver)?, @@ -387,8 +396,12 @@ pub(super) fn finish( } ProxyBooleanStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let result = match target { DirectCallTarget::Callable(callable) => { @@ -478,12 +491,15 @@ mod tests { ); let resume = take_call( resume - .resume(&runtime, Completion::Return(callable)) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(callable).unwrap()), + ) .unwrap(), ); let mut resume = take_descriptor( resume - .resume(&runtime, Completion::Return(Value::Bool(true))) + .resume(&runtime, Completion::Return(JsValue::Bool(true))) .unwrap(), ); if after_descriptor { @@ -515,23 +531,62 @@ mod tests { } } -#[derive(Default)] struct ProxyBooleanStepPending { + runtime: Runtime, delete_object: Option, delete_key: Option, prevent_extensions_object: Option, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, has_object: Option, has_key: Option, extensible_object: Option, descriptor_object: Option, descriptor_key: Option, } +impl ProxyBooleanStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + delete_object: None, + delete_key: None, + prevent_extensions_object: None, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + has_object: None, + has_key: None, + extensible_object: None, + descriptor_object: None, + descriptor_key: None, + } + } +} +impl Drop for ProxyBooleanStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} impl ProxyBooleanStep { pub(crate) fn request_delete( object: ObjectRef, @@ -552,7 +607,7 @@ impl ProxyBooleanStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxyBooleanResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -562,8 +617,8 @@ impl ProxyBooleanStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyBooleanResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -630,7 +685,7 @@ impl ProxyBooleanResume { .take() .expect("ProxyBooleanStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -644,14 +699,14 @@ impl ProxyBooleanResume { .take() .expect("ProxyBooleanStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyBooleanStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/object/internal_methods/call.rs b/src/engine/object/internal_methods/call.rs index a17b35be..6029dbcc 100644 --- a/src/engine/object/internal_methods/call.rs +++ b/src/engine/object/internal_methods/call.rs @@ -7,7 +7,7 @@ use crate::engine::api::{ }; use crate::engine::heap::ContextId; use crate::engine::object::{ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(crate) enum ProxyCallStep { @@ -55,7 +55,7 @@ struct Search { fn overflow(runtime: &Runtime, realm: ContextId) -> Result { Ok(ProxyCallStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Internal, "stack overflow")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Internal, "stack overflow")?, ))) } @@ -96,21 +96,22 @@ impl Search { ))?; if data.is_revoked { return match runtime.proxy_revoked_throw(self.realm)? { - NativeConversion::Throw(value) => { - Ok(ProxyCallStep::Complete(Completion::Throw(value))) - } + NativeConversion::Throw(value) => Ok(ProxyCallStep::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))), NativeConversion::Value(()) => Err(RuntimeError::Invariant( "revoked Proxy call returned a value", )), }; } let rooted = runtime.root_proxy_snapshot(&proxy, data)?; + let read_receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; Ok(ProxyCallStep::request_read( rooted.handler.clone(), self.key.clone(), - Value::Object(rooted.handler.clone()), + read_receiver, ProxyCallResume(Box::new(ProxyCallResumeState { - pending_effect: ProxyCallStepPending::default(), + pending_effect: ProxyCallStepPending::new(runtime.clone()), phase: Phase::Method { rooted, search: self, @@ -138,10 +139,15 @@ impl ProxyCallResume { // Pinned callability validation occurs after the observable trap Get. if !rooted.data.is_callable { return Ok(ProxyCallStep::Complete(Completion::Throw( - runtime.new_native_error(search.realm, NativeErrorKind::Type, "not a function")?, + runtime.new_native_error_jsvalue( + search.realm, + NativeErrorKind::Type, + "not a function", + )?, ))); } - let (target, receiver, arguments) = if matches!(method, Value::Undefined | Value::Null) { + let (target, receiver, arguments) = if matches!(method, JsValue::Undefined | JsValue::Null) + { if runtime.is_proxy_object(&rooted.target)? { search.depth = search.depth.saturating_add(1); return search.read(runtime, rooted.target); @@ -154,11 +160,11 @@ impl ProxyCallResume { } else { // Allocate the argument array before validating the trap, as in C. let array = runtime.new_array_from_values(search.realm, search.arguments)?; - let method = match runtime.direct_call_target_from_value(method) { + let method = match runtime.direct_call_target_from_jsvalue(method) { Ok(method) => method, Err(RuntimeError::Engine(error)) if error.kind() == ErrorKind::Type => { return Ok(ProxyCallStep::Complete(Completion::Throw( - runtime.new_native_error_from_error( + runtime.new_native_error_from_error_jsvalue( search.realm, NativeErrorKind::Type, &error, @@ -177,12 +183,17 @@ impl ProxyCallResume { ], ) }; + let receiver = runtime.into_jsvalue(receiver)?; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; Ok(ProxyCallStep::request_call( target, receiver, arguments, Self(Box::new(ProxyCallResumeState { - pending_effect: ProxyCallStepPending::default(), + pending_effect: ProxyCallStepPending::new(runtime.clone()), phase: Phase::Result { _rooted: rooted, _guard: search.guard, @@ -232,7 +243,7 @@ mod tests { .unwrap(); if after_lookup { step = take_read(step) - .resume(&runtime, Completion::Return(Value::Undefined)) + .resume(&runtime, Completion::Return(JsValue::Undefined)) .unwrap(); } assert_eq!(runtime.0.proxy_method_depth.get(), 1); @@ -253,20 +264,51 @@ mod tests { } } -#[derive(Default)] struct ProxyCallStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, +} +impl ProxyCallStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + } + } +} +impl Drop for ProxyCallStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } } impl ProxyCallStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxyCallResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -276,8 +318,8 @@ impl ProxyCallStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyCallResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -301,7 +343,7 @@ impl ProxyCallResume { .take() .expect("ProxyCallStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -315,14 +357,14 @@ impl ProxyCallResume { .take() .expect("ProxyCallStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyCallStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/object/internal_methods/construct.rs b/src/engine/object/internal_methods/construct.rs index 0ba617f6..5802b45c 100644 --- a/src/engine/object/internal_methods/construct.rs +++ b/src/engine/object/internal_methods/construct.rs @@ -8,7 +8,7 @@ use crate::engine::{ }, heap::ContextId, object::{ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{ Completion, call::{ConstructNewTarget, ConstructorRef, DirectCallTarget}, @@ -60,7 +60,7 @@ struct Search { } fn overflow(runtime: &Runtime, realm: ContextId) -> Result { Ok(ProxyConstructStep::Complete(Completion::Throw( - runtime.new_native_error(realm, NativeErrorKind::Internal, "stack overflow")?, + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Internal, "stack overflow")?, ))) } impl ProxyConstructStep { @@ -103,9 +103,9 @@ impl Search { ))?; if data.is_revoked { return match runtime.proxy_revoked_throw(self.realm)? { - NativeConversion::Throw(value) => { - Ok(ProxyConstructStep::Complete(Completion::Throw(value))) - } + NativeConversion::Throw(value) => Ok(ProxyConstructStep::Complete( + Completion::Throw(runtime.unroot_value(&value)?), + )), NativeConversion::Value(()) => Err(RuntimeError::Invariant( "revoked Proxy construct returned a value", )), @@ -116,7 +116,7 @@ impl Search { rooted.handler.clone(), self.key.clone(), ProxyConstructResume(Box::new(ProxyConstructResumeState { - pending_effect: ProxyConstructStepPending::default(), + pending_effect: ProxyConstructStepPending::new(runtime.clone()), phase: Phase::Method { rooted, search: self, @@ -135,8 +135,8 @@ impl ProxyConstructResume { Phase::Method { rooted, search } => (rooted, search), Phase::Result { realm, trap, .. } => { return Ok(ProxyConstructStep::Complete(match completion { - Completion::Return(value) if trap && !matches!(value, Value::Object(_)) => { - Completion::Throw(runtime.new_native_error( + Completion::Return(value) if trap && !matches!(value, JsValue::Object(_)) => { + Completion::Throw(runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an object", @@ -156,10 +156,12 @@ impl ProxyConstructResume { { NativeConversion::Value(target) => target, NativeConversion::Throw(value) => { - return Ok(ProxyConstructStep::Complete(Completion::Throw(value))); + return Ok(ProxyConstructStep::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))); } }; - if matches!(method, Value::Null | Value::Undefined) { + if matches!(method, JsValue::Null | JsValue::Undefined) { if runtime.is_proxy_object(target.as_object())? { search.depth = search.depth.saturating_add(1); return search.read(runtime, target); @@ -167,9 +169,13 @@ impl ProxyConstructResume { return Ok(ProxyConstructStep::request_construct( target, search.new_target, - search.arguments, + search + .arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?, Self(Box::new(ProxyConstructResumeState { - pending_effect: ProxyConstructStepPending::default(), + pending_effect: ProxyConstructStepPending::new(runtime.clone()), phase: Phase::Result { realm: search.realm, trap: false, @@ -180,11 +186,11 @@ impl ProxyConstructResume { )); } let array = runtime.new_array_from_values(search.realm, search.arguments)?; - let method = match runtime.direct_call_target_from_value(method) { + let method = match runtime.direct_call_target_from_jsvalue(method) { Ok(method) => method, Err(RuntimeError::Engine(error)) if error.kind() == ErrorKind::Type => { return Ok(ProxyConstructStep::Complete(Completion::Throw( - runtime.new_native_error_from_error( + runtime.new_native_error_from_error_jsvalue( search.realm, NativeErrorKind::Type, &error, @@ -195,14 +201,16 @@ impl ProxyConstructResume { }; Ok(ProxyConstructStep::request_call( method, - Value::Object(rooted.handler.clone()), - vec![ - Value::Object(rooted.target.clone()), - Value::Object(array), - search.new_target.value(), - ], + runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?, + [ + runtime.into_jsvalue(Value::Object(rooted.target.clone()))?, + runtime.into_jsvalue(Value::Object(array))?, + runtime.into_jsvalue(runtime.root_value(&search.new_target.value())?)?, + ] + .into_iter() + .collect::>(), Self(Box::new(ProxyConstructResumeState { - pending_effect: ProxyConstructStepPending::default(), + pending_effect: ProxyConstructStepPending::new(runtime.clone()), phase: Phase::Result { realm: search.realm, trap: true, @@ -231,8 +239,12 @@ pub(super) fn finish( } ProxyConstructStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let result = match target { DirectCallTarget::Callable(target) => { @@ -248,7 +260,11 @@ pub(super) fn finish( ProxyConstructStep::Construct { mut resume } => { let target = resume.take_construct_target(); let new_target = resume.take_construct_new_target(); - let arguments = resume.take_construct_arguments(); + let arguments = resume + .take_construct_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; resume.resume( runtime, runtime.construct_internal_with_new_target( @@ -260,16 +276,54 @@ pub(super) fn finish( } } -#[derive(Default)] struct ProxyConstructStepPending { + runtime: Runtime, read_object: Option, read_key: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, construct_target: Option, construct_new_target: Option, - construct_arguments: Option>, + construct_arguments: Option>, +} +impl ProxyConstructStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + call_target: None, + call_receiver: None, + call_arguments: None, + construct_target: None, + construct_new_target: None, + construct_arguments: None, + } + } +} +impl Drop for ProxyConstructStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(values) = self.construct_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(new_target) = self.construct_new_target.take() { + let _ = self.runtime.release_jsvalue(new_target.into_value()); + } + } } impl ProxyConstructStep { pub(crate) fn request_read( @@ -283,8 +337,8 @@ impl ProxyConstructStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyConstructResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -295,7 +349,7 @@ impl ProxyConstructStep { pub(crate) fn request_construct( target: ConstructorRef, new_target: ConstructNewTarget, - arguments: Vec, + arguments: Vec, mut resume: ProxyConstructResume, ) -> Self { resume.0.pending_effect.construct_target = Some(target); @@ -326,14 +380,14 @@ impl ProxyConstructResume { .take() .expect("ProxyConstructStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyConstructStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -354,7 +408,7 @@ impl ProxyConstructResume { .take() .expect("ProxyConstructStep Construct new_target") } - pub(crate) fn take_construct_arguments(&mut self) -> Vec { + pub(crate) fn take_construct_arguments(&mut self) -> Vec { self.0 .pending_effect .construct_arguments diff --git a/src/engine/object/internal_methods/define.rs b/src/engine/object/internal_methods/define.rs index e7732391..d6efd31c 100644 --- a/src/engine/object/internal_methods/define.rs +++ b/src/engine/object/internal_methods/define.rs @@ -11,7 +11,7 @@ use crate::engine::object::{ CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, }; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(crate) enum ProxyDefineStep { @@ -80,7 +80,9 @@ fn method( step: MethodStep, ) -> Result { Ok(match step { - MethodStep::Throw(value) => ProxyDefineStep::Complete(NativeConversion::Throw(value)), + MethodStep::Throw(value) => ProxyDefineStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value.take())?, + )), MethodStep::Read { mut resume } => { let object = resume.take_read_object(); let method_key = resume.take_read_key(); @@ -90,7 +92,7 @@ fn method( method_key, receiver, ProxyDefineResume(Box::new(ProxyDefineResumeState { - pending_effect: ProxyDefineStepPending::default(), + pending_effect: ProxyDefineStepPending::new(runtime.clone()), realm, phase: Phase::Method { resume, @@ -110,7 +112,7 @@ fn method( key, descriptor, ProxyDefineResume(Box::new(ProxyDefineResumeState { - pending_effect: ProxyDefineStepPending::default(), + pending_effect: ProxyDefineStepPending::new(runtime.clone()), realm, phase: Phase::Forward { _rooted: rooted }, })), @@ -118,16 +120,21 @@ fn method( Some(target) => { let key_value = runtime.property_key_value(&key)?; let descriptor_object = runtime.proxy_descriptor_object(realm, &descriptor)?; + let receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; + let arguments = [ + Value::Object(rooted.target.clone()), + key_value, + Value::Object(descriptor_object), + ] + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; ProxyDefineStep::request_call( target, - Value::Object(rooted.handler.clone()), - vec![ - Value::Object(rooted.target.clone()), - key_value, - Value::Object(descriptor_object), - ], + receiver, + arguments, ProxyDefineResume(Box::new(ProxyDefineResumeState { - pending_effect: ProxyDefineStepPending::default(), + pending_effect: ProxyDefineStepPending::new(runtime.clone()), realm, phase: Phase::Trap { rooted, @@ -149,7 +156,9 @@ impl ProxyDefineResume { ) -> Result { let value = match completion { Completion::Throw(value) => { - return Ok(ProxyDefineStep::Complete(NativeConversion::Throw(value))); + return Ok(ProxyDefineStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } Completion::Return(value) => value, }; @@ -170,7 +179,7 @@ impl ProxyDefineResume { key, descriptor, } => { - if !runtime.value_to_boolean(&value)? { + if !runtime.value_to_boolean_jsvalue(&value)? { return Ok(ProxyDefineStep::Complete(NativeConversion::Value( InternalDefineResult::RejectedProxyTrap, ))); @@ -179,7 +188,7 @@ impl ProxyDefineResume { rooted.target.clone(), key, Self(Box::new(ProxyDefineResumeState { - pending_effect: ProxyDefineStepPending::default(), + pending_effect: ProxyDefineStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Invariant { rooted, descriptor }, })), @@ -231,25 +240,61 @@ impl ProxyDefineResume { } } -#[derive(Default)] struct ProxyDefineStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, define_object: Option, define_key: Option, define_descriptor: Option, descriptor_object: Option, descriptor_key: Option, } +impl ProxyDefineStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + define_object: None, + define_key: None, + define_descriptor: None, + descriptor_object: None, + descriptor_key: None, + } + } +} +impl Drop for ProxyDefineStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} impl ProxyDefineStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxyDefineResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -259,8 +304,8 @@ impl ProxyDefineStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyDefineResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -304,7 +349,7 @@ impl ProxyDefineResume { .take() .expect("ProxyDefineStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -318,14 +363,14 @@ impl ProxyDefineResume { .take() .expect("ProxyDefineStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyDefineStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/object/internal_methods/get.rs b/src/engine/object/internal_methods/get.rs index 9b264623..9745faa3 100644 --- a/src/engine/object/internal_methods/get.rs +++ b/src/engine/object/internal_methods/get.rs @@ -7,7 +7,7 @@ use super::{ use crate::engine::api::{error::NativeErrorKind, runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::heap::ContextId; use crate::engine::object::{CompleteOrdinaryPropertyDescriptor, ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(crate) enum ProxyGetStep { @@ -64,7 +64,6 @@ enum Phase { }, Invariant { _rooted: RootedProxy, - result: Value, }, } @@ -103,7 +102,7 @@ fn method( step: MethodStep, ) -> Result { Ok(match step { - MethodStep::Throw(value) => ProxyGetStep::Complete(Completion::Throw(value)), + MethodStep::Throw(value) => ProxyGetStep::Complete(Completion::Throw(value.take())), MethodStep::Complete { mut resume } => { let rooted = resume.take_completed_rooted(); let target = resume.take_completed_target(); @@ -112,9 +111,9 @@ fn method( None => ProxyGetStep::request_read( rooted.target.clone(), key, - receiver, + runtime.into_jsvalue(receiver)?, ProxyGetResume(super::reuse::PooledBox::new(ProxyGetResumeState { - pending_effect: ProxyGetStepPending::default(), + pending_effect: ProxyGetStepPending::new(runtime.clone()), realm, phase: Phase::Forward { _rooted: rooted }, })), @@ -122,12 +121,17 @@ fn method( Some(target) => { let key_value = runtime.property_key_value(&key)?; arguments.extend([Value::Object(rooted.target.clone()), key_value, receiver]); + let receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; ProxyGetStep::request_call( target, - Value::Object(rooted.handler.clone()), + receiver, arguments, ProxyGetResume(super::reuse::PooledBox::new(ProxyGetResumeState { - pending_effect: ProxyGetStepPending::default(), + pending_effect: ProxyGetStepPending::new(runtime.clone()), realm, phase: Phase::Trap { rooted, key }, })), @@ -144,7 +148,7 @@ fn method( method_key, method_receiver, ProxyGetResume(super::reuse::PooledBox::new(ProxyGetResumeState { - pending_effect: ProxyGetStepPending::default(), + pending_effect: ProxyGetStepPending::new(runtime.clone()), realm, phase: Phase::Method { resume, @@ -190,16 +194,15 @@ impl ProxyGetResume { // descriptor round. A Proxy target may re-enter JavaScript and // keeps the descriptor round. if runtime.is_proxy_object(&rooted.target)? { + let mut pending = ProxyGetStepPending::new(runtime.clone()); + pending.invariant_result = Some(value); return Ok(ProxyGetStep::request_descriptor( rooted.target.clone(), key, Self(super::reuse::PooledBox::new(ProxyGetResumeState { - pending_effect: ProxyGetStepPending::default(), + pending_effect: pending, realm, - phase: Phase::Invariant { - _rooted: rooted, - result: value, - }, + phase: Phase::Invariant { _rooted: rooted }, })), )); } @@ -218,12 +221,17 @@ impl ProxyGetResume { runtime: &Runtime, descriptor: NativeConversion>, ) -> Result { - let state = self.0.into_inner(); - let Phase::Invariant { _rooted, result } = state.phase else { + let mut state = self.0.into_inner(); + let Phase::Invariant { _rooted } = state.phase else { return Err(RuntimeError::Invariant( "Proxy Get value continuation received a descriptor reply", )); }; + let result = state + .pending_effect + .invariant_result + .take() + .expect("ProxyGetStep Invariant result"); complete_get_invariant(runtime, state.realm, result, descriptor) } } @@ -254,26 +262,30 @@ fn get_invariant_violation( fn complete_get_invariant( runtime: &Runtime, realm: ContextId, - result: Value, + result: JsValue, descriptor: NativeConversion>, ) -> Result { let descriptor = match descriptor { NativeConversion::Value(descriptor) => descriptor, NativeConversion::Throw(value) => { - return Ok(ProxyGetStep::Complete(Completion::Throw(value))); + let _ = runtime.release_jsvalue(result); + return Ok(ProxyGetStep::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))); } }; - Ok(ProxyGetStep::Complete( - if get_invariant_violation(&result, &descriptor) { - Completion::Throw(runtime.new_native_error( + if get_invariant_violation(&runtime.root_value(&result)?, &descriptor) { + let _ = runtime.release_jsvalue(result); + Ok(ProxyGetStep::Complete(Completion::Throw( + runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "proxy: inconsistent get", - )?) - } else { - Completion::Return(result) - }, - )) + )?, + ))) + } else { + Ok(ProxyGetStep::Complete(Completion::Return(result))) + } } #[cfg(test)] @@ -351,22 +363,60 @@ mod tests { } } -#[derive(Default)] struct ProxyGetStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, descriptor_object: Option, descriptor_key: Option, + invariant_result: Option, +} +impl ProxyGetStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + descriptor_object: None, + descriptor_key: None, + invariant_result: None, + } + } +} +impl Drop for ProxyGetStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(value) = self.invariant_result.take() { + let _ = self.runtime.release_jsvalue(value); + } + } } impl ProxyGetStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxyGetResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -376,8 +426,8 @@ impl ProxyGetStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyGetResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -410,7 +460,7 @@ impl ProxyGetResume { .take() .expect("ProxyGetStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -424,14 +474,14 @@ impl ProxyGetResume { .take() .expect("ProxyGetStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyGetStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/object/internal_methods/method.rs b/src/engine/object/internal_methods/method.rs index ea808c27..8cf3ce42 100644 --- a/src/engine/object/internal_methods/method.rs +++ b/src/engine/object/internal_methods/method.rs @@ -8,15 +8,40 @@ use crate::engine::api::{ }; use crate::engine::heap::ContextId; use crate::engine::object::{ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(super) enum MethodStep { Complete { resume: MethodResume }, - Throw(Value), + Throw(MethodThrow), Read { resume: MethodResume }, } +/// Owns the error thrown by method lookup. Abandoned lookups release the edge +/// through `Drop`; every consumer drains it with [`MethodThrow::take`]. +pub(super) struct MethodThrow { + runtime: Runtime, + value: Option, +} +impl MethodThrow { + fn new(runtime: Runtime, value: JsValue) -> Self { + Self { + runtime, + value: Some(value), + } + } + pub(super) fn take(mut self) -> JsValue { + self.value.take().expect("MethodStep Throw value") + } +} +impl Drop for MethodThrow { + fn drop(&mut self) { + if let Some(value) = self.value.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} + pub(super) struct MethodResume(super::reuse::PooledBox); impl std::ops::Deref for MethodResume { type Target = MethodResumeState; @@ -85,11 +110,10 @@ impl MethodStep { } fn overflow(runtime: &Runtime, realm: ContextId) -> Result { - Ok(MethodStep::Throw(runtime.new_native_error( - realm, - NativeErrorKind::Internal, - "stack overflow", - )?)) + Ok(MethodStep::Throw(MethodThrow::new( + runtime.clone(), + runtime.new_native_error_jsvalue(realm, NativeErrorKind::Internal, "stack overflow")?, + ))) } impl Search { @@ -107,7 +131,10 @@ impl Search { else { unreachable!("revoked proxy throws") }; - return Ok(MethodStep::Throw(value)); + return Ok(MethodStep::Throw(MethodThrow::new( + runtime.clone(), + runtime.unroot_value(&value)?, + ))); } // A cached data-slot location skips the dynamic `handler[name]` read. // The value is always read from today's slot, so a same-shape overwrite @@ -118,20 +145,21 @@ impl Search { { let rooted = runtime.root_proxy_snapshot(&proxy, data)?; let resume = MethodResume(super::reuse::PooledBox::new(MethodResumeState { - pending_effect: MethodStepPending::default(), + pending_effect: MethodStepPending::new(runtime.clone()), rooted: Some(rooted), selected: None, search: self, })); - return resume.resume(runtime, Completion::Return(value)); + return resume.resume(runtime, Completion::Return(runtime.unroot_value(&value)?)); } let rooted = runtime.root_proxy_snapshot(&proxy, data)?; + let receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; Ok(MethodStep::request_read( rooted.handler.clone(), self.key.clone(), - Value::Object(rooted.handler.clone()), + receiver, MethodResume(super::reuse::PooledBox::new(MethodResumeState { - pending_effect: MethodStepPending::default(), + pending_effect: MethodStepPending::new(runtime.clone()), rooted: Some(rooted), selected: None, search: self, @@ -153,13 +181,15 @@ impl MethodResume { completion: Completion, ) -> Result { let mut value = match completion { - Completion::Throw(value) => return Ok(MethodStep::Throw(value)), + Completion::Throw(value) => { + return Ok(MethodStep::Throw(MethodThrow::new(runtime.clone(), value))); + } Completion::Return(value) => value, }; // Undefined/Null keeps walking the target Proxy chain iteratively; every // level first tries the trap cache and otherwise keeps the dynamic read. loop { - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { let target = self.0.rooted.as_ref().expect("proxy owner").target.clone(); let Some(data) = runtime.proxy_snapshot_if_any(&target)? else { return Ok(MethodStep::Complete { resume: self }); @@ -182,7 +212,10 @@ impl MethodResume { else { unreachable!("revoked proxy throws") }; - return Ok(MethodStep::Throw(value)); + return Ok(MethodStep::Throw(MethodThrow::new( + runtime.clone(), + runtime.unroot_value(&value)?, + ))); } let next = runtime.root_proxy_snapshot(&target, data)?; let cached = runtime.proxy_trap_read( @@ -195,7 +228,7 @@ impl MethodResume { match cached { Some(method_value) => { drop(old); - value = method_value; + value = runtime.unroot_value(&method_value)?; continue; } None => { @@ -203,7 +236,7 @@ impl MethodResume { let rooted = self.0.rooted.as_ref().expect("proxy owner"); ( rooted.handler.clone(), - Value::Object(rooted.handler.clone()), + runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?, self.0.search.key.clone(), ) }; @@ -213,14 +246,15 @@ impl MethodResume { } } } - let method = match runtime.direct_call_target_from_value(value) { + let method = match runtime.direct_call_target_from_jsvalue(value) { Ok(method) => method, Err(RuntimeError::Engine(error)) if error.kind() == ErrorKind::Type => { - return Ok(MethodStep::Throw(runtime.new_native_error_from_error( + let value = runtime.new_native_error_from_error_jsvalue( self.0.search.realm, NativeErrorKind::Type, &error, - )?)); + )?; + return Ok(MethodStep::Throw(MethodThrow::new(runtime.clone(), value))); } Err(error) => return Err(error), }; @@ -230,17 +264,36 @@ impl MethodResume { } } -#[derive(Default)] struct MethodStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, +} +impl MethodStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + } + } +} +impl Drop for MethodStepPending { + /// Release the internal read edge still held when the request is + /// abandoned. Consumption goes through `Option::take`. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + } } impl MethodStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: MethodResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -264,7 +317,7 @@ impl MethodResume { .take() .expect("MethodStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -297,7 +350,7 @@ mod resident_tests { drop(resume.take_read_key()); drop(resume.take_read_receiver()); let MethodStep::Complete { mut resume } = resume - .resume(&runtime, Completion::Return(Value::Undefined)) + .resume(&runtime, Completion::Return(JsValue::Undefined)) .unwrap() else { panic!("complete") diff --git a/src/engine/object/internal_methods/own_keys.rs b/src/engine/object/internal_methods/own_keys.rs index fcda14ce..ab0de660 100644 --- a/src/engine/object/internal_methods/own_keys.rs +++ b/src/engine/object/internal_methods/own_keys.rs @@ -8,7 +8,7 @@ use crate::engine::{ atom::Atom, heap::ContextId, object::{CompleteOrdinaryPropertyDescriptor, ObjectRef, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::{Completion, call::DirectCallTarget}, }; use std::collections::HashSet; @@ -87,26 +87,32 @@ impl KeysStep { realm: ContextId, object: ObjectRef, ) -> Result { - method(realm, MethodStep::start(runtime, realm, object, "ownKeys")?) + method( + runtime, + realm, + MethodStep::start(runtime, realm, object, "ownKeys")?, + ) } } -fn method(realm: ContextId, step: MethodStep) -> Result { +fn method(runtime: &Runtime, realm: ContextId, step: MethodStep) -> Result { Ok(match step { MethodStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let _ = resume.take_read_receiver(); + runtime.release_jsvalue(resume.take_read_receiver())?; KeysStep::request_read( - Value::Object(object), + runtime.into_jsvalue(Value::Object(object))?, key, KeysResume(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Method(resume), })), ) } - MethodStep::Throw(value) => KeysStep::Complete(NativeConversion::Throw(value)), + MethodStep::Throw(value) => KeysStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value.take())?, + )), MethodStep::Complete { mut resume } => { let rooted = resume.take_completed_rooted(); let target = resume.take_completed_target(); @@ -115,17 +121,17 @@ fn method(realm: ContextId, step: MethodStep) -> Result None => KeysStep::request_keys( rooted.target.clone(), KeysResume(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Forward(rooted), })), ), Some(target) => KeysStep::request_call( target, - Value::Object(rooted.handler.clone()), - vec![Value::Object(rooted.target.clone())], + runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?, + vec![runtime.into_jsvalue(Value::Object(rooted.target.clone()))?], KeysResume(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Trap(rooted), })), @@ -150,10 +156,10 @@ fn items( if keys.len() < length as usize { let key = runtime.intern_property_key(&keys.len().to_string())?; return Ok(KeysStep::request_read( - list.clone(), + runtime.into_jsvalue(list.clone())?, key, KeysResume(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Item { rooted, @@ -177,7 +183,7 @@ fn items( Ok(KeysStep::request_extensible( rooted.target.clone(), KeysResume(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Extensible { rooted, @@ -200,7 +206,7 @@ fn check_next( state.rooted.target.clone(), key.clone(), KeysResume(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Descriptor { state, key }, })), @@ -224,30 +230,34 @@ impl KeysResume { let value = match result { Completion::Return(value) => value, Completion::Throw(value) => { - return Ok(KeysStep::Complete(NativeConversion::Throw(value))); + return Ok(KeysStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } }; let realm = self.0.realm; match self.0.phase { - Phase::Method(resume) => { - method(realm, resume.resume(runtime, Completion::Return(value))?) + Phase::Method(resume) => method( + runtime, + realm, + resume.resume(runtime, Completion::Return(value))?, + ), + Phase::Trap(rooted) => { + let list = runtime.root_and_release_jsvalue(value)?; + Ok(KeysStep::request_read( + runtime.into_jsvalue(list.clone())?, + runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, + Self(Box::new(KeysResumeState { + pending_effect: KeysStepPending::new(runtime.clone()), + realm, + phase: Phase::Length { rooted, list }, + })), + )) } - Phase::Trap(rooted) => Ok(KeysStep::request_read( - value.clone(), - runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Length)?, - Self(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), - realm, - phase: Phase::Length { - rooted, - list: value, - }, - })), - )), Phase::Length { rooted, list } => Ok(KeysStep::request_number( value, Self(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm, phase: Phase::Number { rooted, list }, })), @@ -258,7 +268,7 @@ impl KeysResume { length, mut keys, } => { - let key = match value { + let key = match runtime.root_and_release_jsvalue(value)? { Value::String(value) => runtime.intern_property_key_js_string(&value)?, Value::Symbol(value) => PropertyKey::from(value), _ => { @@ -325,7 +335,7 @@ impl KeysResume { Ok(KeysStep::request_keys( rooted.target.clone(), Self(Box::new(KeysResumeState { - pending_effect: KeysStepPending::default(), + pending_effect: KeysStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::TargetKeys { rooted, @@ -412,7 +422,7 @@ pub(super) fn finish( step = match step { KeysStep::Complete(result) => return Ok(result), KeysStep::Read { mut resume } => { - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; let key = resume.take_read_key(); resume.resume( runtime, @@ -421,8 +431,12 @@ pub(super) fn finish( } KeysStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let completion = match target { DirectCallTarget::Callable(callable) => { @@ -436,7 +450,7 @@ pub(super) fn finish( } } KeysStep::Number { mut resume } => { - let value = resume.take_number_value(); + let value = runtime.root_and_release_jsvalue(resume.take_number_value())?; resume.number(runtime, runtime.native_to_number(realm, &value)?)? } KeysStep::Keys { mut resume } => { @@ -480,7 +494,10 @@ mod tests { panic!("expected handler read") }; let KeysStep::Call { resume, .. } = resume - .resume(&runtime, Completion::Return(callable)) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(callable).unwrap()), + ) .unwrap() else { panic!("expected trap") @@ -488,13 +505,16 @@ mod tests { let list = runtime.new_object(None).unwrap(); let list_id = list.object_id(); let KeysStep::Read { resume, .. } = resume - .resume(&runtime, Completion::Return(Value::Object(list))) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(Value::Object(list)).unwrap()), + ) .unwrap() else { panic!("expected length") }; let KeysStep::Number { mut resume, .. } = resume - .resume(&runtime, Completion::Return(Value::Int(1))) + .resume(&runtime, Completion::Return(JsValue::Int(1))) .unwrap() else { panic!("expected conversion") @@ -512,7 +532,10 @@ mod tests { panic!("expected item") }; let KeysStep::Extensible { resume: next, .. } = next - .resume(&runtime, Completion::Return(Value::Symbol(symbol))) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(Value::Symbol(symbol)).unwrap()), + ) .unwrap() else { panic!("expected target query") @@ -564,29 +587,71 @@ mod tests { } } -#[derive(Default)] struct KeysStepPending { - read_receiver: Option, + runtime: Runtime, + read_receiver: Option, read_key: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, - number_value: Option, + call_receiver: Option, + call_arguments: Option>, + number_value: Option, keys_object: Option, extensible_object: Option, descriptor_object: Option, descriptor_key: Option, } +impl KeysStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_receiver: None, + read_key: None, + call_target: None, + call_receiver: None, + call_arguments: None, + number_value: None, + keys_object: None, + extensible_object: None, + descriptor_object: None, + descriptor_key: None, + } + } +} +impl Drop for KeysStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(value) = self.number_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} impl KeysStep { - pub(crate) fn request_read(receiver: Value, key: PropertyKey, mut resume: KeysResume) -> Self { + pub(crate) fn request_read( + receiver: JsValue, + key: PropertyKey, + mut resume: KeysResume, + ) -> Self { resume.0.pending_effect.read_receiver = Some(receiver); resume.0.pending_effect.read_key = Some(key); Self::Read { resume } } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: KeysResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -594,7 +659,7 @@ impl KeysStep { resume.0.pending_effect.call_arguments = Some(arguments); Self::Call { resume } } - pub(crate) fn request_number(value: Value, mut resume: KeysResume) -> Self { + pub(crate) fn request_number(value: JsValue, mut resume: KeysResume) -> Self { resume.0.pending_effect.number_value = Some(value); Self::Number { resume } } @@ -617,7 +682,7 @@ impl KeysStep { } } impl KeysResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -638,21 +703,21 @@ impl KeysResume { .take() .expect("KeysStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("KeysStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments .take() .expect("KeysStep Call arguments") } - pub(crate) fn take_number_value(&mut self) -> Value { + pub(crate) fn take_number_value(&mut self) -> JsValue { self.0 .pending_effect .number_value diff --git a/src/engine/object/internal_methods/own_property.rs b/src/engine/object/internal_methods/own_property.rs index 89245c1a..faeaec26 100644 --- a/src/engine/object/internal_methods/own_property.rs +++ b/src/engine/object/internal_methods/own_property.rs @@ -13,7 +13,7 @@ use crate::engine::object::property::validate_and_apply_property_descriptor; use crate::engine::object::{ CompleteOrdinaryPropertyDescriptor, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, }; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; type Descriptor = Option; @@ -64,7 +64,6 @@ enum Phase { }, Extensible { rooted: RootedProxy, - result: Value, target: Descriptor, }, Converted { @@ -94,7 +93,9 @@ fn method( step: MethodStep, ) -> Result { Ok(match step { - MethodStep::Throw(value) => ProxyOwnStep::Complete(NativeConversion::Throw(value)), + MethodStep::Throw(value) => ProxyOwnStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value.take())?, + )), MethodStep::Complete { mut resume } => { let rooted = resume.take_completed_rooted(); let target = resume.take_completed_target(); @@ -104,19 +105,24 @@ fn method( rooted.target.clone(), key, ProxyOwnResume(Box::new(ProxyOwnResumeState { - pending_effect: ProxyOwnStepPending::default(), + pending_effect: ProxyOwnStepPending::new(runtime.clone()), realm, phase: Phase::Forward { _rooted: rooted }, })), ), Some(target) => { let key_value = runtime.property_key_value(&key)?; + let receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; + let arguments = [Value::Object(rooted.target.clone()), key_value] + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; ProxyOwnStep::request_call( target, - Value::Object(rooted.handler.clone()), - vec![Value::Object(rooted.target.clone()), key_value], + receiver, + arguments, ProxyOwnResume(Box::new(ProxyOwnResumeState { - pending_effect: ProxyOwnStepPending::default(), + pending_effect: ProxyOwnStepPending::new(runtime.clone()), realm, phase: Phase::Trap { rooted, key }, })), @@ -133,7 +139,7 @@ fn method( method_key, receiver, ProxyOwnResume(Box::new(ProxyOwnResumeState { - pending_effect: ProxyOwnStepPending::default(), + pending_effect: ProxyOwnStepPending::new(runtime.clone()), realm, phase: Phase::Method { resume, key }, })), @@ -150,7 +156,9 @@ impl ProxyOwnResume { ) -> Result { let value = match completion { Completion::Throw(value) => { - return Ok(ProxyOwnStep::Complete(NativeConversion::Throw(value))); + return Ok(ProxyOwnStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } Completion::Return(value) => value, }; @@ -162,7 +170,7 @@ impl ProxyOwnResume { resume.resume(runtime, Completion::Return(value))?, ), Phase::Trap { rooted, key } => { - if !matches!(value, Value::Undefined | Value::Object(_)) { + if !matches!(value, JsValue::Undefined | JsValue::Object(_)) { return Ok(ProxyOwnStep::Complete(runtime.proxy_invariant_throw( self.0.realm, "getOwnPropertyDescriptor", @@ -172,11 +180,11 @@ impl ProxyOwnResume { rooted.target.clone(), key, Self(Box::new(ProxyOwnResumeState { - pending_effect: ProxyOwnStepPending::default(), + pending_effect: ProxyOwnStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Target { rooted, - result: value, + result: runtime.root_and_release_jsvalue(value)?, }, })), )) @@ -215,16 +223,14 @@ impl ProxyOwnResume { } // QuickJS queries target extensibility before reading any // fields from the descriptor returned by the trap. + let mut pending = ProxyOwnStepPending::new(runtime.clone()); + pending.extensible_result = Some(runtime.into_jsvalue(result)?); Ok(ProxyOwnStep::request_extensible( rooted.target.clone(), Self(Box::new(ProxyOwnResumeState { - pending_effect: ProxyOwnStepPending::default(), + pending_effect: pending, realm: self.0.realm, - phase: Phase::Extensible { - rooted, - result, - target, - }, + phase: Phase::Extensible { rooted, target }, })), )) } @@ -238,16 +244,19 @@ impl ProxyOwnResume { self, result: NativeConversion, ) -> Result { - let Phase::Extensible { - rooted, - result: value, - target, - } = self.0.phase - else { + let mut state = self.0; + let value = state + .pending_effect + .extensible_result + .take() + .expect("ProxyOwnStep Extensible result"); + let realm = state.realm; + let Phase::Extensible { rooted, target } = state.phase else { return Err(RuntimeError::Invariant( "Proxy descriptor continuation received an extensibility reply", )); }; + let runtime = rooted.proxy.runtime().clone(); match result { NativeConversion::Throw(value) => { Ok(ProxyOwnStep::Complete(NativeConversion::Throw(value))) @@ -255,8 +264,8 @@ impl ProxyOwnResume { NativeConversion::Value(extensible) => Ok(ProxyOwnStep::request_convert( value, Self(Box::new(ProxyOwnResumeState { - pending_effect: ProxyOwnStepPending::default(), - realm: self.0.realm, + pending_effect: ProxyOwnStepPending::new(runtime), + realm, phase: Phase::Converted { _rooted: rooted, target, @@ -312,24 +321,67 @@ impl ProxyOwnResume { } } -#[derive(Default)] struct ProxyOwnStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, descriptor_object: Option, descriptor_key: Option, extensible_object: Option, - convert_value: Option, + extensible_result: Option, + convert_value: Option, +} +impl ProxyOwnStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + descriptor_object: None, + descriptor_key: None, + extensible_object: None, + extensible_result: None, + convert_value: None, + } + } +} +impl Drop for ProxyOwnStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(value) = self.extensible_result.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.convert_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + } } impl ProxyOwnStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxyOwnResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -339,8 +391,8 @@ impl ProxyOwnStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyOwnResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -361,7 +413,7 @@ impl ProxyOwnStep { resume.0.pending_effect.extensible_object = Some(object); Self::Extensible { resume } } - pub(crate) fn request_convert(value: Value, mut resume: ProxyOwnResume) -> Self { + pub(crate) fn request_convert(value: JsValue, mut resume: ProxyOwnResume) -> Self { resume.0.pending_effect.convert_value = Some(value); Self::Convert { resume } } @@ -381,7 +433,7 @@ impl ProxyOwnResume { .take() .expect("ProxyOwnStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -395,14 +447,14 @@ impl ProxyOwnResume { .take() .expect("ProxyOwnStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyOwnStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -430,7 +482,7 @@ impl ProxyOwnResume { .take() .expect("ProxyOwnStep Extensible object") } - pub(crate) fn take_convert_value(&mut self) -> Value { + pub(crate) fn take_convert_value(&mut self) -> JsValue { self.0 .pending_effect .convert_value diff --git a/src/engine/object/internal_methods/prototype.rs b/src/engine/object/internal_methods/prototype.rs index 7bdc9c4b..e219fafd 100644 --- a/src/engine/object/internal_methods/prototype.rs +++ b/src/engine/object/internal_methods/prototype.rs @@ -6,7 +6,7 @@ use super::{ use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::heap::ContextId; use crate::engine::object::{ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(crate) enum ProxyPrototypeKind { @@ -91,13 +91,13 @@ impl ProxyPrototypeStep { } } fn method( - _runtime: &Runtime, + runtime: &Runtime, realm: ContextId, kind: ProxyPrototypeKind, step: MethodStep, ) -> Result { Ok(match step { - MethodStep::Throw(value) => ProxyPrototypeStep::Complete(Completion::Throw(value)), + MethodStep::Throw(value) => ProxyPrototypeStep::Complete(Completion::Throw(value.take())), MethodStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); @@ -107,7 +107,7 @@ fn method( key, receiver, ProxyPrototypeResume(Box::new(ProxyPrototypeResumeState { - pending_effect: ProxyPrototypeStepPending::default(), + pending_effect: ProxyPrototypeStepPending::new(runtime.clone()), realm, phase: Phase::Method { resume, kind }, })), @@ -125,7 +125,7 @@ fn method( _ => None, }; let resume = ProxyPrototypeResume(Box::new(ProxyPrototypeResumeState { - pending_effect: ProxyPrototypeStepPending::default(), + pending_effect: ProxyPrototypeStepPending::new(runtime.clone()), realm, phase: Phase::Forward { _rooted: rooted, @@ -144,12 +144,17 @@ fn method( if let ProxyPrototypeKind::Set(prototype) = &kind { arguments.push(prototype.clone().map_or(Value::Null, Value::Object)); } + let receiver = runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; + let arguments = arguments + .into_iter() + .map(|value| runtime.into_jsvalue(value)) + .collect::, _>>()?; ProxyPrototypeStep::request_call( target, - Value::Object(rooted.handler.clone()), + receiver, arguments, ProxyPrototypeResume(Box::new(ProxyPrototypeResumeState { - pending_effect: ProxyPrototypeStepPending::default(), + pending_effect: ProxyPrototypeStepPending::new(runtime.clone()), realm, phase: Phase::Trap { rooted, kind }, })), @@ -161,15 +166,17 @@ fn method( } fn completed(prototype: Option, setting: bool) -> ProxyPrototypeStep { ProxyPrototypeStep::Complete(Completion::Return(if setting { - Value::Bool(true) + JsValue::Bool(true) } else { - prototype.map_or(Value::Null, Value::Object) + prototype.map_or(JsValue::Null, |object| { + JsValue::Object(object.into_handle()) + }) })) } fn inconsistent(runtime: &Runtime, realm: ContextId) -> Result { Ok(ProxyPrototypeStep::Complete( match runtime.proxy_invariant_throw::(realm, "prototype")? { - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.unroot_value(&value)?), NativeConversion::Value(_) => { return Err(RuntimeError::Invariant( "Proxy invariant rejection returned a value", @@ -201,16 +208,18 @@ impl ProxyPrototypeResume { let (prototype, setting) = match kind { ProxyPrototypeKind::Get => ( match value { - Value::Object(object) => Some(object), - Value::Null => None, + JsValue::Object(object) => { + Some(ObjectRef::from_owned_handle(runtime.clone(), object)) + } + JsValue::Null => None, _ => return inconsistent(runtime, self.0.realm), }, false, ), ProxyPrototypeKind::Set(prototype) => { - if !runtime.value_to_boolean(&value)? { + if !runtime.value_to_boolean_jsvalue(&value)? { return Ok(ProxyPrototypeStep::Complete(Completion::Return( - Value::Bool(false), + JsValue::Bool(false), ))); } (prototype, true) @@ -219,7 +228,7 @@ impl ProxyPrototypeResume { Ok(ProxyPrototypeStep::request_extensible( rooted.target.clone(), Self(Box::new(ProxyPrototypeResumeState { - pending_effect: ProxyPrototypeStepPending::default(), + pending_effect: ProxyPrototypeStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Extensible { rooted, @@ -236,13 +245,15 @@ impl ProxyPrototypeResume { } pub(crate) fn boolean( self, - _runtime: &Runtime, + runtime: &Runtime, result: NativeConversion, ) -> Result { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ProxyPrototypeStep::Complete(Completion::Throw(value))); + return Ok(ProxyPrototypeStep::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))); } }; match self.0.phase { @@ -250,7 +261,7 @@ impl ProxyPrototypeResume { kind: ProxyPrototypeKind::Set(_), .. } => Ok(ProxyPrototypeStep::Complete(Completion::Return( - Value::Bool(value), + JsValue::Bool(value), ))), Phase::Extensible { rooted, @@ -263,7 +274,7 @@ impl ProxyPrototypeResume { Ok(ProxyPrototypeStep::request_get( rooted.target.clone(), Self(Box::new(ProxyPrototypeResumeState { - pending_effect: ProxyPrototypeStepPending::default(), + pending_effect: ProxyPrototypeStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Compare { _rooted: rooted, @@ -286,7 +297,9 @@ impl ProxyPrototypeResume { let value = match result { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(ProxyPrototypeStep::Complete(Completion::Throw(value))); + return Ok(ProxyPrototypeStep::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))); } }; match self.0.phase { @@ -321,7 +334,7 @@ pub(super) fn finish( ProxyPrototypeStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = runtime.root_and_release_jsvalue(resume.take_read_receiver())?; resume.resume( runtime, runtime.internal_get(realm, &object, &key, receiver)?, @@ -329,8 +342,12 @@ pub(super) fn finish( } ProxyPrototypeStep::Call { mut resume } => { let target = resume.take_call_target(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = runtime.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>()?; { let result = match target { DirectCallTarget::Callable(callable) => { @@ -415,7 +432,10 @@ mod tests { ); let resume = take_call( resume - .resume(&runtime, Completion::Return(callable)) + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(callable).unwrap()), + ) .unwrap(), ); let reply = if setting { @@ -424,8 +444,14 @@ mod tests { } else { Value::Object(prototype) }; - let mut resume = - take_extensible(resume.resume(&runtime, Completion::Return(reply)).unwrap()); + let mut resume = take_extensible( + resume + .resume( + &runtime, + Completion::Return(runtime.into_jsvalue(reply).unwrap()), + ) + .unwrap(), + ); if compare { resume = take_get( resume @@ -451,24 +477,59 @@ mod tests { } } -#[derive(Default)] struct ProxyPrototypeStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, get_object: Option, set_object: Option, set_prototype: Option>, extensible_object: Option, } +impl ProxyPrototypeStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + get_object: None, + set_object: None, + set_prototype: None, + extensible_object: None, + } + } +} +impl Drop for ProxyPrototypeStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + } +} impl ProxyPrototypeStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxyPrototypeResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -478,8 +539,8 @@ impl ProxyPrototypeStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxyPrototypeResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -520,7 +581,7 @@ impl ProxyPrototypeResume { .take() .expect("ProxyPrototypeStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -534,14 +595,14 @@ impl ProxyPrototypeResume { .take() .expect("ProxyPrototypeStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxyPrototypeStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/object/internal_methods/set.rs b/src/engine/object/internal_methods/set.rs index 306bac66..1c968a6d 100644 --- a/src/engine/object/internal_methods/set.rs +++ b/src/engine/object/internal_methods/set.rs @@ -7,7 +7,7 @@ use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::heap::ContextId; use crate::engine::object::operations::InternalSetResult; use crate::engine::object::{CompleteOrdinaryPropertyDescriptor, ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{Completion, call::DirectCallTarget}; pub(crate) enum ProxySetStep { @@ -80,7 +80,9 @@ fn method( step: MethodStep, ) -> Result { Ok(match step { - MethodStep::Throw(value) => ProxySetStep::Complete(NativeConversion::Throw(value)), + MethodStep::Throw(value) => ProxySetStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value.take())?, + )), MethodStep::Read { mut resume } => { let object = resume.take_read_object(); let method_key = resume.take_read_key(); @@ -90,7 +92,7 @@ fn method( method_key, method_receiver, ProxySetResume(Box::new(ProxySetResumeState { - pending_effect: ProxySetStepPending::default(), + pending_effect: ProxySetStepPending::new(runtime.clone()), realm, phase: Phase::Method { resume, @@ -109,27 +111,33 @@ fn method( None => ProxySetStep::request_set( rooted.target.clone(), key, - value, - receiver, + runtime.into_jsvalue(value)?, + runtime.into_jsvalue(receiver)?, ProxySetResume(Box::new(ProxySetResumeState { - pending_effect: ProxySetStepPending::default(), + pending_effect: ProxySetStepPending::new(runtime.clone()), realm, phase: Phase::Forward { _rooted: rooted }, })), ), Some(target) => { let key_value = runtime.property_key_value(&key)?; + let call_receiver = + runtime.into_jsvalue(Value::Object(rooted.handler.clone()))?; + let receiver = runtime.into_jsvalue(receiver)?; + let arguments = [ + runtime.into_jsvalue(Value::Object(rooted.target.clone()))?, + runtime.into_jsvalue(key_value)?, + runtime.into_jsvalue(value.clone())?, + receiver, + ] + .into_iter() + .collect::>(); ProxySetStep::request_call( target, - Value::Object(rooted.handler.clone()), - vec![ - Value::Object(rooted.target.clone()), - key_value, - value.clone(), - receiver, - ], + call_receiver, + arguments, ProxySetResume(Box::new(ProxySetResumeState { - pending_effect: ProxySetStepPending::default(), + pending_effect: ProxySetStepPending::new(runtime.clone()), realm, phase: Phase::Trap { rooted, key, value }, })), @@ -147,7 +155,9 @@ impl ProxySetResume { ) -> Result { let result = match completion { Completion::Throw(value) => { - return Ok(ProxySetStep::Complete(NativeConversion::Throw(value))); + return Ok(ProxySetStep::Complete(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + ))); } Completion::Return(value) => value, }; @@ -166,7 +176,7 @@ impl ProxySetResume { resume.resume(runtime, Completion::Return(result))?, ), Phase::Trap { rooted, key, value } => { - if !runtime.value_to_boolean(&result)? { + if !runtime.value_to_boolean_jsvalue(&result)? { return Ok(ProxySetStep::Complete(NativeConversion::Value( InternalSetResult::RejectedProxyTrap, ))); @@ -175,7 +185,7 @@ impl ProxySetResume { rooted.target.clone(), key, Self(Box::new(ProxySetResumeState { - pending_effect: ProxySetStepPending::default(), + pending_effect: ProxySetStepPending::new(runtime.clone()), realm: self.0.realm, phase: Phase::Invariant { _rooted: rooted, @@ -238,26 +248,69 @@ impl ProxySetResume { } } -#[derive(Default)] struct ProxySetStepPending { + runtime: Runtime, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, call_target: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, set_object: Option, set_key: Option, - set_value: Option, - set_receiver: Option, + set_value: Option, + set_receiver: Option, descriptor_object: Option, descriptor_key: Option, } +impl ProxySetStepPending { + fn new(runtime: Runtime) -> Self { + Self { + runtime, + read_object: None, + read_key: None, + read_receiver: None, + call_target: None, + call_receiver: None, + call_arguments: None, + set_object: None, + set_key: None, + set_value: None, + set_receiver: None, + descriptor_object: None, + descriptor_key: None, + } + } +} +impl Drop for ProxySetStepPending { + /// Release the internal edges still held when the request is abandoned. + /// Consumption goes through `Option::take`; releases are defer-safe and + /// nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.call_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(values) = self.call_arguments.take() { + for value in values { + let _ = self.runtime.release_jsvalue(value); + } + } + if let Some(value) = self.set_value.take() { + let _ = self.runtime.release_jsvalue(value); + } + if let Some(value) = self.set_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + } +} impl ProxySetStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: ProxySetResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -267,8 +320,8 @@ impl ProxySetStep { } pub(crate) fn request_call( target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: ProxySetResume, ) -> Self { resume.0.pending_effect.call_target = Some(target); @@ -279,8 +332,8 @@ impl ProxySetStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, - receiver: Value, + value: JsValue, + receiver: JsValue, mut resume: ProxySetResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -314,7 +367,7 @@ impl ProxySetResume { .take() .expect("ProxySetStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -328,14 +381,14 @@ impl ProxySetResume { .take() .expect("ProxySetStep Call target") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("ProxySetStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments @@ -356,14 +409,14 @@ impl ProxySetResume { .take() .expect("ProxySetStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value .take() .expect("ProxySetStep Set value") } - pub(crate) fn take_set_receiver(&mut self) -> Value { + pub(crate) fn take_set_receiver(&mut self) -> JsValue { self.0 .pending_effect .set_receiver diff --git a/src/engine/object/mod.rs b/src/engine/object/mod.rs index aa0efbec..da504adb 100644 --- a/src/engine/object/mod.rs +++ b/src/engine/object/mod.rs @@ -85,6 +85,21 @@ impl ObjectRef { pub(crate) const fn object_id(&self) -> ObjectId { self.id } + + /// Consume this root and transfer its owned reference to the caller. + /// + /// The edge count is unchanged: this root retains the edge so its own + /// release on drop nets to a transfer. `ObjectRef::drop` still runs, so the + /// owned runtime handle is disposed instead of leaked. A live root always + /// resolves, mirroring [`ObjectRef::clone`]'s invariant treatment. + #[must_use] + pub(crate) fn into_handle(self) -> ObjectId { + let id = self.id; + self.runtime + .retain_object_handle(id) + .expect("transferring a live object root must retain its handle"); + id + } } impl Clone for ObjectRef { @@ -182,6 +197,20 @@ impl AtomOwner { fn domain_id(&self) -> u64 { self.runtime.domain_id() } + + /// Consume this root and transfer its owned atom reference to the caller. + /// + /// The edge count is unchanged: the atom is retained here so this owner's + /// release on drop nets to a transfer, and the runtime handle is disposed + /// normally. A live root always resolves. + #[must_use] + fn into_atom(self) -> Atom { + let atom = self.atom; + self.runtime + .retain_atom_handle(atom) + .expect("transferring a live atom root must retain its handle"); + atom + } } impl Clone for AtomOwner { @@ -374,11 +403,6 @@ impl WellKnownSymbol { pub struct SymbolRef(AtomOwner); impl SymbolRef { - /// Fallible retain for an owning VM slot, without deferred work or GC. - pub(crate) fn try_clone(&self) -> Result { - self.0.try_clone().map(Self) - } - /// Consume one already-owned, symbol-kind-validated atom reference. #[must_use] pub(crate) const fn from_owned_atom(runtime: Runtime, atom: Atom) -> Self { @@ -419,6 +443,13 @@ impl SymbolRef { pub(crate) const fn atom(&self) -> Atom { self.0.atom() } + + /// Consume this root, transferring its one owned atom reference to the + /// caller without retaining or releasing. + #[must_use] + pub(crate) fn into_atom(self) -> Atom { + self.0.into_atom() + } } impl From for PropertyKey { diff --git a/src/engine/object/object_literal/element.rs b/src/engine/object/object_literal/element.rs index 867de5c1..8ad7b2af 100644 --- a/src/engine/object/object_literal/element.rs +++ b/src/engine/object/object_literal/element.rs @@ -5,7 +5,7 @@ use crate::engine::{ object::{ ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, operations::InternalDefineResult, }, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, vm::Completion, }; @@ -16,10 +16,11 @@ pub(crate) enum LiteralDefinitionStep { } pub(crate) struct LiteralDefinitionResume(Box); struct LiteralDefinitionState { + runtime: Runtime, realm: Option, object: Option, value: Option, - primitive: Option, + primitive: Option, key: Option, descriptor: Option, } @@ -31,8 +32,10 @@ impl LiteralDefinitionStep { key: PropertyKey, descriptor: OrdinaryPropertyDescriptor, ) -> Self { + let runtime = object.runtime().clone(); Self::Define { resume: LiteralDefinitionResume(Box::new(LiteralDefinitionState { + runtime, realm: None, object: Some(object), value: None, @@ -52,10 +55,11 @@ impl LiteralDefinitionStep { if matches!(key, Value::Object(_)) { Ok(Self::Primitive { resume: LiteralDefinitionResume(Box::new(LiteralDefinitionState { + runtime: runtime.clone(), realm: Some(realm), object: Some(object), value: Some(value), - primitive: Some(key), + primitive: Some(runtime.into_jsvalue(key)?), key: None, descriptor: None, })), @@ -67,13 +71,15 @@ impl LiteralDefinitionStep { key, Runtime::public_class_field_descriptor(value), )), - NativeConversion::Throw(value) => Ok(Self::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => Ok(Self::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))), } } } } impl LiteralDefinitionResume { - pub(crate) fn take_primitive(&mut self) -> Value { + pub(crate) fn take_primitive(&mut self) -> JsValue { self.0.primitive.take().expect("literal primitive request") } pub(crate) fn take_define(&mut self) -> (ObjectRef, PropertyKey, OrdinaryPropertyDescriptor) { @@ -97,10 +103,12 @@ impl LiteralDefinitionResume { let realm = self.0.realm.take().ok_or(RuntimeError::Invariant( "literal definition lost its key conversion owner", ))?; - let key = match runtime.property_key_from_primitive(realm, key)? { + let key = match runtime.property_key_from_primitive_jsvalue(realm, key)? { NativeConversion::Value(key) => key, NativeConversion::Throw(value) => { - return Ok(LiteralDefinitionStep::Complete(Completion::Throw(value))); + return Ok(LiteralDefinitionStep::Complete(Completion::Throw( + runtime.unroot_value(&value)?, + ))); } }; self.0.key = Some(key); @@ -118,11 +126,12 @@ impl LiteralDefinitionResume { "literal definition reply has wrong owner", )); } + let runtime = self.0.runtime.clone(); Ok(LiteralDefinitionStep::Complete(match result { NativeConversion::Value(InternalDefineResult::Defined) => { - Completion::Return(Value::Undefined) + Completion::Return(crate::engine::value::JsValue::Undefined) } - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.unroot_value(&value)?), NativeConversion::Value(InternalDefineResult::RejectedOrdinary(_)) => { return Err(Error::new(ErrorKind::Type, "property is not configurable").into()); } @@ -161,9 +170,17 @@ mod resident_tests { panic!("primitive request") }; let address = (&*resume.0) as *const LiteralDefinitionState; - assert_eq!(resume.take_primitive(), key); + assert_eq!( + runtime + .root_and_release_jsvalue(resume.take_primitive()) + .unwrap(), + key + ); let LiteralDefinitionStep::Define { mut resume } = resume - .resume(&runtime, Completion::Return(primitive)) + .resume( + &runtime, + Completion::Return(runtime.unroot_value(&primitive).unwrap()), + ) .unwrap() else { panic!("define request") @@ -176,7 +193,9 @@ mod resident_tests { resume .defined(NativeConversion::Value(InternalDefineResult::Defined)) .unwrap(), - LiteralDefinitionStep::Complete(Completion::Return(Value::Undefined)) + LiteralDefinitionStep::Complete(Completion::Return( + crate::engine::value::JsValue::Undefined + )) )); } } diff --git a/src/engine/object/ordinary.rs b/src/engine/object/ordinary.rs index 8a7a6291..20d2f475 100644 --- a/src/engine/object/ordinary.rs +++ b/src/engine/object/ordinary.rs @@ -12,8 +12,8 @@ use crate::engine::object::{ CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, }; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; mod set; @@ -102,11 +102,27 @@ impl Runtime { ) -> Result>, RuntimeError> { use crate::engine::vm::Completion; match read { - OrdinaryRead::Complete(value) => Ok(NativeConversion::Value(value)), + OrdinaryRead::Complete(value) => { + // Boundary rooting: the internal value's edges are duplicated + // into the public root, then its own edges are released. + let value = match value { + Some(value) => { + let rooted = self.root_value(&value)?; + self.release_jsvalue(value)?; + Some(rooted) + } + None => None, + }; + Ok(NativeConversion::Value(value)) + } OrdinaryRead::Call { getter, receiver } => { Ok(match self.call_internal(realm, &getter, receiver, &[])? { - Completion::Return(value) => NativeConversion::Value(Some(value)), - Completion::Throw(value) => NativeConversion::Throw(value), + Completion::Return(value) => { + NativeConversion::Value(Some(self.root_and_release_jsvalue(value)?)) + } + Completion::Throw(value) => { + NativeConversion::Throw(self.root_and_release_jsvalue(value)?) + } }) } OrdinaryRead::Special { @@ -154,9 +170,12 @@ impl Runtime { loop { let current = prototype.as_ref().unwrap_or(object); match self.ordinary_read_probe_selected(current, key, native.as_deref_mut())? { - ReadProbe::Value(value) => return Ok(OrdinaryRead::Complete(Some(value))), + ReadProbe::Value(value) => { + let value = self.unroot_value(&value)?; + return Ok(OrdinaryRead::Complete(Some(value))); + } ReadProbe::Getter(None) => { - return Ok(OrdinaryRead::Complete(Some(Value::Undefined))); + return Ok(OrdinaryRead::Complete(Some(JsValue::Undefined))); } ReadProbe::Getter(Some(getter)) => { return Ok(OrdinaryRead::Call { @@ -187,7 +206,7 @@ impl Runtime { Value::Undefined } }; - return Ok(OrdinaryRead::Complete(Some(value))); + return Ok(OrdinaryRead::Complete(Some(self.unroot_value(&value)?))); } // Reuse the full storage kernel for Array holes, String, // Arguments, namespace live cells and lazy own properties. @@ -195,7 +214,7 @@ impl Runtime { if let Some(own) = self.get_own_property_in_operation(current, key)? { return Ok(match own { CompleteOrdinaryPropertyDescriptor::Data { value, .. } => { - OrdinaryRead::Complete(Some(value)) + OrdinaryRead::Complete(Some(self.unroot_value(&value)?)) } CompleteOrdinaryPropertyDescriptor::Accessor { get: Some(getter), @@ -205,7 +224,7 @@ impl Runtime { receiver: receiver.clone(), }, CompleteOrdinaryPropertyDescriptor::Accessor { get: None, .. } => { - OrdinaryRead::Complete(Some(Value::Undefined)) + OrdinaryRead::Complete(Some(JsValue::Undefined)) } }); } @@ -223,8 +242,10 @@ impl Runtime { } /// A rooted ordinary lookup result, ready for an explicit caller to consume. +/// The completed data value travels as an internal value; accessor receivers +/// stay public roots because host callbacks consume them. pub(crate) enum OrdinaryRead { - Complete(Option), + Complete(Option), Call { getter: crate::engine::object::CallableRef, receiver: Value, diff --git a/src/engine/object/ordinary/set.rs b/src/engine/object/ordinary/set.rs index cb4c2c7f..cf9667c8 100644 --- a/src/engine/object/ordinary/set.rs +++ b/src/engine/object/ordinary/set.rs @@ -61,10 +61,54 @@ pub(crate) struct SetResumeState { phase: Phase, request_object: Option, request_key: Option, - request_value: Option, - request_receiver: Option, + request: SetRequestEdges, request_descriptor: Option, } + +/// The request value/receiver edges with the runtime that must release any +/// leftover owner when a phase is abandoned. Keeping the pair in one field +/// lets the surrounding resume state stay movable. +struct SetRequestEdges { + runtime: Runtime, + value: Option, + receiver: Option, +} + +impl SetRequestEdges { + fn new(runtime: &Runtime) -> Self { + Self { + runtime: runtime.clone(), + value: None, + receiver: None, + } + } + + fn set(&mut self, value: JsValue, receiver: JsValue) { + self.value = Some(value); + self.receiver = Some(receiver); + } + + fn take_value(&mut self) -> JsValue { + self.value.take().expect("selected Set request field") + } + + fn take_receiver(&mut self) -> JsValue { + self.receiver.take().expect("selected Set request field") + } +} + +impl Drop for SetRequestEdges { + /// `take_*` transfers ownership first, so completed transitions drop empty + /// options; releases are defer-safe and never run JavaScript. + fn drop(&mut self) { + for value in [self.value.take(), self.receiver.take()] + .into_iter() + .flatten() + { + let _ = self.runtime.release_jsvalue(value); + } + } +} enum Phase { Walk(ObjectRef), Forward, @@ -269,16 +313,17 @@ impl SetStep { if !matches!( resume .0 - .request_value + .request + .value .as_ref() .expect("selected Set request field"), - Value::Object(_) + JsValue::Object(_) ) => { let object = resume.take_object(); let key = resume.take_key(); - let value = resume.take_value(); - let receiver = resume.take_receiver(); + let value = runtime.root_and_release_jsvalue(resume.take_value())?; + let receiver = runtime.root_and_release_jsvalue(resume.take_receiver())?; let realm = resume .state .realm @@ -309,15 +354,16 @@ impl SetStep { if !matches!( resume .0 - .request_value + .request + .value .as_ref() .expect("selected Set request field"), - Value::Object(_) + JsValue::Object(_) ) => { let object = resume.take_object(); let key = resume.take_key(); - let value = resume.take_value(); + let value = runtime.root_and_release_jsvalue(resume.take_value())?; let action = runtime.prepare_set_array_length( resume.state.realm, &object, @@ -422,8 +468,8 @@ impl SetStep { Self::Proxy { mut resume } => { let object = resume.take_object(); let key = resume.take_key(); - let value = resume.take_value(); - let receiver = resume.take_receiver(); + let value = runtime.root_and_release_jsvalue(resume.take_value())?; + let receiver = runtime.root_and_release_jsvalue(resume.take_receiver())?; let realm = resume .state .realm @@ -434,8 +480,8 @@ impl SetStep { Self::Special { mut resume } => { let object = resume.take_object(); let key = resume.take_key(); - let value = resume.take_value(); - let receiver = resume.take_receiver(); + let value = runtime.root_and_release_jsvalue(resume.take_value())?; + let receiver = runtime.root_and_release_jsvalue(resume.take_receiver())?; let realm = resume .state .realm @@ -453,7 +499,7 @@ impl SetStep { Self::ArrayLength { mut resume } => { let object = resume.take_object(); let key = resume.take_key(); - let value = resume.take_value(); + let value = runtime.root_and_release_jsvalue(resume.take_value())?; let action = runtime.prepare_set_array_length(resume.state.realm, &object, &key, value)?; resume.forward(action) @@ -534,7 +580,7 @@ fn start_waiting( match selected { SelectedSet::Complete(action) => Ok(Some(action)), selected => { - waiting(state.publish_selected(selected)?); + waiting(state.publish_selected(runtime, selected)?); Ok(None) } } @@ -915,7 +961,11 @@ impl State { } } - fn publish_selected(self, selected: SelectedSet) -> Result { + fn publish_selected( + self, + runtime: &Runtime, + selected: SelectedSet, + ) -> Result { if let SelectedSet::Complete(action) = selected { return complete(action); } @@ -924,18 +974,21 @@ impl State { phase: Phase::Forward, request_object: None, request_key: None, - request_value: None, - request_receiver: None, + request: SetRequestEdges::new(runtime), request_descriptor: None, })) - .publish_selected(selected) + .publish_selected(runtime, selected) } } impl SetResume { // Effect owners live in the same continuation allocation across local and // scheduler transitions. SetStep transports only the phase and pointer. - fn publish_selected(mut self, selected: SelectedSet) -> Result { + fn publish_selected( + mut self, + runtime: &Runtime, + selected: SelectedSet, + ) -> Result { #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event(match &selected { SelectedSet::Complete(_) => "set_completion_adapter", @@ -955,23 +1008,28 @@ impl SetResume { SelectedSet::Proxy(object) => { self.0.request_object = Some(object); self.0.request_key = Some(clone_set_key(&self.0.state.key)); - self.0.request_value = Some(clone_set_value(&self.0.state.value)); - self.0.request_receiver = Some(clone_set_value(&self.0.state.receiver)); + self.0.request.set( + runtime.into_jsvalue(clone_set_value(&self.0.state.value))?, + runtime.into_jsvalue(clone_set_value(&self.0.state.receiver))?, + ); self.0.phase = Phase::Forward; Ok(SetStep::Proxy { resume: self }) } SelectedSet::Special(object) => { self.0.request_object = Some(clone_set_object(&object)); self.0.request_key = Some(clone_set_key(&self.0.state.key)); - self.0.request_value = Some(clone_set_value(&self.0.state.value)); - self.0.request_receiver = Some(clone_set_value(&self.0.state.receiver)); + self.0.request.set( + runtime.into_jsvalue(clone_set_value(&self.0.state.value))?, + runtime.into_jsvalue(clone_set_value(&self.0.state.receiver))?, + ); self.0.phase = Phase::Special(object); Ok(SetStep::Special { resume: self }) } SelectedSet::ArrayLength(object) => { self.0.request_object = Some(object); self.0.request_key = Some(clone_set_key(&self.0.state.key)); - self.0.request_value = Some(clone_set_value(&self.0.state.value)); + self.0.request.value = + Some(runtime.into_jsvalue(clone_set_value(&self.0.state.value))?); self.0.phase = Phase::Forward; Ok(SetStep::ArrayLength { resume: self }) } @@ -1002,17 +1060,11 @@ impl SetResume { .take() .expect("selected Set request field") } - pub(crate) fn take_value(&mut self) -> Value { - self.0 - .request_value - .take() - .expect("selected Set request field") + pub(crate) fn take_value(&mut self) -> JsValue { + self.0.request.take_value() } - pub(crate) fn take_receiver(&mut self) -> Value { - self.0 - .request_receiver - .take() - .expect("selected Set request field") + pub(crate) fn take_receiver(&mut self) -> JsValue { + self.0.request.take_receiver() } pub(crate) fn take_descriptor(&mut self) -> OrdinaryPropertyDescriptor { self.0 @@ -1026,7 +1078,7 @@ impl SetResume { selected: SelectedSet, ) -> Result { let selected = self.0.state.advance_selected(runtime, selected)?; - self.publish_selected(selected) + self.publish_selected(runtime, selected) } pub(crate) fn array_length( @@ -1129,9 +1181,9 @@ impl Runtime { use crate::engine::vm::Completion; match result { NativeConversion::Value(InternalSetResult::Accepted) => { - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } - NativeConversion::Value(_) if !strict => Ok(Completion::Return(Value::Undefined)), + NativeConversion::Value(_) if !strict => Ok(Completion::Return(JsValue::Undefined)), NativeConversion::Value(InternalSetResult::RejectedProxyTrap) => { Err(Error::new(ErrorKind::Type, "proxy: cannot set property").into()) } @@ -1162,7 +1214,7 @@ impl Runtime { NativeConversion::Value(InternalSetResult::Rejected( PropertySetRejection::NotObject, )) => Err(Error::new(ErrorKind::Type, "not an object").into()), - NativeConversion::Throw(value) => Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => Ok(Completion::Throw(self.unroot_value(&value)?)), } } } diff --git a/src/engine/object/ordinary_storage.rs b/src/engine/object/ordinary_storage.rs index 79be1f2e..e293def2 100644 --- a/src/engine/object/ordinary_storage.rs +++ b/src/engine/object/ordinary_storage.rs @@ -4,12 +4,12 @@ mod ic; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::heap::runtime::RuntimeState; 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; +use crate::engine::value::{JsValue, Value}; /// Affine native payload fact selected together with an own property value. /// Its callee remains retained by the result/operand owner; consumption checks @@ -29,6 +29,7 @@ impl LinkedNativeSelection { } } +#[derive(Clone, Copy)] struct OwnSlot { index: usize, flags: PropertyFlags, @@ -50,7 +51,7 @@ fn locate( ) -> Result, RuntimeError> { let data = state.heap.object(object)?; let shape = state.heap.shape(data.shape)?; - let Some(index) = shape.find(atom) else { + let Some(index) = shape.find(AtomIdx::from_raw(atom.raw())) else { return Ok(None); }; let index = index as usize; @@ -95,7 +96,7 @@ fn select_set_slot( && shape .entries() .first() - .is_some_and(|entry| entry.atom != atom) + .is_some_and(|entry| entry.atom != AtomIdx::from_raw(atom.raw())) } _ => false, }; @@ -181,8 +182,13 @@ pub(super) fn prototypes_allow_dense_append( /// Continue an already-selected missing own property without releasing the /// borrow. Any exotic boundary declines before changing the receiver; the /// ordinary state machine then performs its original observable protocol. +/// +/// A `Define` result means the define is validated and still missing: the +/// caller ends the borrow, converts the value (which may allocate a +/// string/BigInt node and needs the state borrow), and commits through +/// [`Runtime::store_property_slot`]. No callback or mutation can run between +/// this selection and that commit, so splitting the borrow is unobservable. fn set_missing_local( - runtime: &Runtime, state: &mut RuntimeState, receiver: ObjectId, atom: Atom, @@ -218,19 +224,7 @@ fn set_missing_local( crate::engine::object::operations::PropertySetRejection::NotExtensible, ))); } - let replacement = PropertySlot::Data(runtime.raw_property_value(value)?); - state.store_selected_property_slot( - receiver, - atom, - PropertyFlags::data(true, true, true), - replacement, - None, - )?; - #[cfg(feature = "profiling")] - crate::engine::api::profiling::record_owned_execution_event( - "set_missing_committed_from_selection", - ); - Ok(MissingSelection::Complete(SetProbe::Stored(true))) + Ok(MissingSelection::Define) } #[derive(Clone, Copy)] @@ -307,6 +301,13 @@ impl Runtime { DenseAppend(u32), SpecialAt(ObjectId, SpecialKind), + + /// A validated missing define on the target: commit after the + /// borrow, once the value conversion can take it. + Define, + + /// A selected writable own data slot awaiting replacement. + DataReplace(OwnSlot), } let selected = { let mut state = self.0.state.borrow_mut(); @@ -347,7 +348,6 @@ impl Runtime { BorrowedSet::Missing(prototype) => { if receiver_is_target { match set_missing_local( - self, &mut state, id, key.atom(), @@ -358,9 +358,7 @@ impl Runtime { MissingSelection::Special(id, kind) => { Selected::SpecialAt(id, kind) } - MissingSelection::Define => { - unreachable!("missing receiver definition is consumed locally") - } + MissingSelection::Define => Selected::Define, } } else { Selected::Missing(prototype) @@ -373,9 +371,7 @@ impl Runtime { 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)); + Selected::DataReplace(slot) } BorrowedSet::Setter(set) => Selected::Setter(set), BorrowedSet::Special(kind) => return Ok(SetProbe::Special(kind)), @@ -402,6 +398,44 @@ impl Runtime { Some(reason) => SetProbe::Rejected(reason), } } + Selected::Define => { + // The define was validated under the selection borrow; convert + // outside it (node allocation needs the borrow) and commit. + let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); + let stored = self.store_property_slot( + object, + key, + PropertyFlags::data(true, true, true), + PropertySlot::Data(raw), + ); + self.release_converted_value_edge(&conversion_probe); + stored?; + #[cfg(feature = "profiling")] + crate::engine::api::profiling::record_owned_execution_event( + "set_missing_committed_from_selection", + ); + SetProbe::Stored(true) + } + Selected::DataReplace(slot) => { + let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); + let mut state = self.0.state.borrow_mut(); + let replaced = replace_data( + &mut state, + object.object_id(), + slot, + PropertySlot::Data(raw), + ); + drop(state); + self.release_converted_value_edge(&conversion_probe); + replaced?; + return Ok(SetProbe::Stored(true)); + } Selected::Setter(set) => SetProbe::Setter(set), Selected::Missing(prototype) => SetProbe::Missing( prototype @@ -635,28 +669,73 @@ impl Runtime { { return Ok(None); } + // Convert before taking the state borrow: node allocation needs it. + let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); let mut state = self.0.state.borrow_mut(); let id = object.object_id(); - if !is_ordinary(state.heap.object(id)?) { + let ordinary = match state.heap.object(id) { + Ok(data) => is_ordinary(data), + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error.into()); + } + }; + if !ordinary { + drop(state); + self.release_converted_value_edge(&conversion_probe); return Ok(None); } - let Some(slot) = locate(&state, id, key.atom())? else { - return Ok(None); + let slot = match locate(&state, id, key.atom()) { + Ok(slot) => slot, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } }; - let PropertySlot::Data(old) = &state.heap.object(id)?.slots[slot.index] else { + let Some(slot) = slot else { + drop(state); + self.release_converted_value_edge(&conversion_probe); return Ok(None); }; - let raw = self.raw_property_value(value)?; + let old = match state.heap.object(id) { + Ok(data) => match &data.slots[slot.index] { + PropertySlot::Data(old) => old, + _ => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Ok(None); + } + }, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error.into()); + } + }; if !crate::engine::object::property::data_value_update_allowed( slot.flags.configurable, slot.flags.writable, old, &raw, - crate::engine::value::collection_key::same_value, + |left, right| { + crate::engine::value::collection_key::same_value(&state.heap, left, right) + }, ) { + drop(state); + self.release_converted_value_edge(&conversion_probe); return Ok(Some(false)); } - replace_data(&mut state, id, slot, PropertySlot::Data(raw))?; + let replaced = replace_data(&mut state, id, slot, PropertySlot::Data(raw)); + drop(state); + // The slot retained its own copy edge on success; a rejected update + // kept nothing. Balance the producer edge either way. + self.release_converted_value_edge(&conversion_probe); + replaced?; Ok(Some(true)) } } @@ -865,6 +944,20 @@ fn immediate_value(raw: &crate::engine::heap::RawValue) -> Option { }) } +/// Scalar projection for an immediate leaf result: the returned internal +/// value owns no heap edge, matching [`immediate_value`]. +fn immediate_value_jsvalue(raw: &crate::engine::heap::RawValue) -> Option { + use crate::engine::heap::RawValue; + Some(match raw { + RawValue::Undefined => JsValue::Undefined, + RawValue::Null => JsValue::Null, + RawValue::Bool(value) => JsValue::Bool(*value), + RawValue::Int(value) => JsValue::Int(*value), + RawValue::Float(value) => JsValue::Float(*value), + _ => return None, + }) +} + fn linked_field_atom( runtime: &Runtime, executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, @@ -887,20 +980,20 @@ impl Runtime { /// every decline leaves input owners, lazy properties and prototypes alone. pub(crate) fn try_ordinary_field_immediate_read( &self, - base: &Value, + base: &JsValue, executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, - ) -> Option { + ) -> Option { use crate::engine::heap::SlotReleaseReadiness; let atom = linked_field_atom(self, executable, index)?; if !matches!( - self.slot_value_release_readiness(base), + self.slot_value_release_readiness_jsvalue(base), Ok(SlotReleaseReadiness::Ready) ) { return None; } let state = self.0.state.try_borrow().ok()?; - if let Value::String(string) = base { + if let JsValue::String(id) = base { let info = state.atoms.resolve(atom).ok()?; let crate::engine::atom::AtomSpelling::Text(name) = info.spelling else { return None; @@ -908,22 +1001,26 @@ impl Runtime { return (info.kind == crate::engine::atom::AtomKind::String && name.len() == 6 && name.utf16_units().eq("length".encode_utf16())) - .then(|| Value::number(string.len() as f64)); + .then(|| JsValue::Float(state.heap.string_fast(*id).len() as f64)); } - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return None; }; - let id = object.object_id(); + let id = *object; let data = state.heap.object(id).ok()?; if matches!( (data.kind, &data.payload), (ObjectKind::Array, ObjectPayload::Array { .. }) ) { let first = state.heap.shape(data.shape).ok()?.entries().first()?; - if first.atom == atom { + if first.atom == AtomIdx::from_raw(atom.raw()) { let (length, _) = Self::array_length_state_in_heap(&state.heap, id, atom).ok()??; - return Some(Self::array_length_value(length)); + return Some(if let Ok(length) = i32::try_from(length) { + JsValue::Int(length) + } else { + JsValue::Float(f64::from(length)) + }); } return None; } @@ -934,7 +1031,7 @@ impl Runtime { let PropertySlot::Data(value) = &data.slots[slot.index] else { return None; }; - immediate_value(value) + immediate_value_jsvalue(value) } /// A published function already owns its static key. Only the selected /// result/getter is promoted here; fallback will acquire an owning key. @@ -945,12 +1042,15 @@ impl Runtime { executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, ) -> Result, RuntimeError> { - self.prepare_linked_own_read_selected(base, executable, index, None) + let internal = self.unroot_value(base)?; + let result = self.prepare_linked_own_read_selected(&internal, executable, index, None); + self.release_jsvalue(internal)?; + result } pub(crate) fn prepare_linked_own_read_selected( &self, - base: &Value, + base: &JsValue, executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, native: Option<&mut Option>, @@ -958,21 +1058,25 @@ impl Runtime { let Some(atom) = linked_field_atom(self, executable, index) else { return Ok(None); }; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return Ok(None); }; let _operation = self.operation(); - self.validate_value_domain(base, "property receiver")?; + let object = crate::engine::object::ObjectRef::from_borrowed_handle(self.clone(), *object)?; Ok( - match self.ordinary_read_probe_atom(object, atom, true, native)? { + match self.ordinary_read_probe_atom(&object, atom, true, native)? { ReadProbe::Value(value) => { - Some(crate::engine::object::OrdinaryRead::Complete(Some(value))) + // Transfer the probed root into the internal value without + // a retain/release pair. + Some(crate::engine::object::OrdinaryRead::Complete(Some( + self.into_jsvalue(value)?, + ))) } ReadProbe::Getter(None) => Some(crate::engine::object::OrdinaryRead::Complete( - Some(Value::Undefined), + Some(JsValue::Undefined), )), ReadProbe::Getter(Some(getter)) => { - let receiver = base.clone(); + let receiver = self.root_value(base)?; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( "linked_read_owner_clone.ReceiverObject", @@ -988,26 +1092,30 @@ impl Runtime { #[cfg(test)] pub(crate) fn try_ordinary_field_immediate_write( &self, - base: &Value, + base: &JsValue, executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, - value: &Value, + value: &JsValue, ) -> bool { use crate::engine::heap::SlotReleaseReadiness; if !matches!( value, - Value::Undefined | Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) ) { return false; } let Some(atom) = linked_field_atom(self, executable, index) else { return false; }; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return false; }; if !matches!( - self.slot_value_release_readiness(base), + self.slot_value_release_readiness_jsvalue(base), Ok(SlotReleaseReadiness::Ready) ) { return false; @@ -1015,7 +1123,7 @@ impl Runtime { let Ok(mut state) = self.0.state.try_borrow_mut() else { return false; }; - let id = object.object_id(); + let id = *object; let Ok(data) = state.heap.object(id) else { return false; }; @@ -1034,9 +1142,9 @@ impl Runtime { if immediate_value(old).is_none() { return false; } - let Ok(raw) = self.raw_property_value(value) else { - return false; - }; + // `value` was matched to a scalar above, so this id copy allocates + // nothing and never takes the state borrow the caller still holds. + let raw = value.as_raw(); // The canonical transaction validates shape storage before publication. // With scalar old/new values it retains/releases no edges or atoms; // the already-empty zero queue makes post-commit cleanup infallible. @@ -1050,55 +1158,58 @@ impl Runtime { /// Every decline leaves owners and storage untouched; the general property /// lookup retains all missing/exotic/reference-valued cases. #[cfg(test)] - pub(crate) fn try_dense_array_immediate_read(&self, base: &Value, index: u32) -> Option { + pub(crate) fn try_dense_array_immediate_read( + &self, + base: &JsValue, + index: u32, + ) -> Option { self.try_array_immediate_read_kind(base, index, false) } /// One release proof and heap borrow select either existing array kernel. - pub(crate) fn try_array_immediate_read(&self, base: &Value, index: u32) -> Option { + pub(crate) fn try_array_immediate_read(&self, base: &JsValue, index: u32) -> Option { self.try_array_immediate_read_kind(base, index, true) } fn try_array_immediate_read_kind( &self, - base: &Value, + base: &JsValue, index: u32, include_typed: bool, - ) -> Option { + ) -> Option { use crate::engine::heap::SlotReleaseReadiness; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return None; }; if !matches!( - self.slot_value_release_readiness(base), + self.slot_value_release_readiness_jsvalue(base), Ok(SlotReleaseReadiness::Ready) ) { return None; } let mut state = self.0.state.try_borrow_mut().ok()?; - let data = state.heap.object(object.object_id()).ok()?; + let data = state.heap.object(*object).ok()?; if matches!( (data.kind, &data.payload), (ObjectKind::Array, ObjectPayload::Array { .. }) ) { - return immediate_value(data.dense_array_value(index)?); + return immediate_value_jsvalue(data.dense_array_value(index)?); } if !include_typed { return None; } if matches!(data.payload, ObjectPayload::Arguments { .. }) { let atom = Atom::from_immediate_integer(index)?; - let slot = locate(&state, object.object_id(), atom).ok()??; + let slot = locate(&state, *object, atom).ok()??; return match &data.slots[slot.index] { - PropertySlot::Data(value) => immediate_value(value), + PropertySlot::Data(value) => immediate_value_jsvalue(value), PropertySlot::VarRef(cell) => { - immediate_value(&state.heap.var_ref(*cell).ok()?.value) + immediate_value_jsvalue(&state.heap.var_ref(*cell).ok()?.value) } PropertySlot::Accessor { .. } | PropertySlot::AutoInit(_) => None, }; } - let value = - Self::typed_array_number_read_in_heap(&mut state.heap, object.object_id(), index)?; + let value = Self::typed_array_number_read_in_heap(&mut state.heap, *object, index)?; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event("typed_array_number_read_leaf"); Some(value) @@ -1120,17 +1231,15 @@ mod dense_array_read_tests { #[test] fn dense_array_read_leaf_reads_scalars_without_changing_owners() { let runtime = Runtime::new(); - let base = receiver(&runtime, "[undefined,null,true,17,-0,NaN]"); - let keep = base.clone(); - let Value::Object(object) = &base else { + let base = runtime + .into_jsvalue(receiver(&runtime, "[undefined,null,true,17,-0,NaN]")) + .unwrap(); + let keep = runtime.dup_jsvalue(&base).unwrap(); + let JsValue::Object(object) = &base else { panic!("array"); }; - let count = runtime - .0 - .state - .borrow() - .heap - .object_strong_count(object.object_id()); + let object = *object; + let count = runtime.0.state.borrow().heap.object_strong_count(object); for (index, expected) in [ Value::Undefined, Value::Null, @@ -1144,31 +1253,24 @@ mod dense_array_read_tests { let result = runtime .try_dense_array_immediate_read(&base, index as u32) .unwrap(); - assert!(result.same_quickjs_representation(expected)); + assert!( + runtime + .root_value(&result) + .unwrap() + .same_quickjs_representation(expected) + ); } assert!( - matches!(runtime.try_dense_array_immediate_read(&base, 5), Some(Value::Float(v)) if v.is_nan()) + matches!(runtime.try_dense_array_immediate_read(&base, 5), Some(JsValue::Float(v)) if v.is_nan()) ); assert_eq!( - runtime - .0 - .state - .borrow() - .heap - .object_strong_count(object.object_id()), + runtime.0.state.borrow().heap.object_strong_count(object), count ); - drop(keep); + runtime.release_jsvalue(keep).unwrap(); assert!(runtime.try_dense_array_immediate_read(&base, 0).is_none()); - assert!( - runtime - .0 - .state - .borrow() - .heap - .object(object.object_id()) - .is_ok() - ); + assert!(runtime.0.state.borrow().heap.object(object).is_ok()); + runtime.release_jsvalue(base).unwrap(); } #[test] @@ -1186,44 +1288,30 @@ mod dense_array_read_tests { "({0:1,length:1})", ] { let runtime = Runtime::new(); - let base = receiver(&runtime, expression); - let keep = base.clone(); - let Value::Object(object) = &base else { + let base = runtime + .into_jsvalue(receiver(&runtime, expression)) + .unwrap(); + let keep = runtime.dup_jsvalue(&base).unwrap(); + let JsValue::Object(object) = &base else { panic!("object"); }; - let count = runtime - .0 - .state - .borrow() - .heap - .object_strong_count(object.object_id()); + let object = *object; + let count = runtime.0.state.borrow().heap.object_strong_count(object); assert!( runtime.try_dense_array_immediate_read(&base, 0).is_none(), "{expression}" ); assert_eq!( - runtime - .0 - .state - .borrow() - .heap - .object_strong_count(object.object_id()), + runtime.0.state.borrow().heap.object_strong_count(object), count ); - assert!( - runtime - .0 - .state - .borrow() - .heap - .object(object.object_id()) - .is_ok() - ); - drop(keep); + assert!(runtime.0.state.borrow().heap.object(object).is_ok()); + runtime.release_jsvalue(keep).unwrap(); + runtime.release_jsvalue(base).unwrap(); } let runtime = Runtime::new(); - let base = receiver(&runtime, "[1]"); - let _keep = base.clone(); + let base = runtime.into_jsvalue(receiver(&runtime, "[1]")).unwrap(); + let keep = runtime.dup_jsvalue(&base).unwrap(); assert!(runtime.try_dense_array_immediate_read(&base, 1).is_none()); assert!( runtime @@ -1232,14 +1320,20 @@ mod dense_array_read_tests { ); let other = Runtime::new(); assert!(other.try_dense_array_immediate_read(&base, 0).is_none()); + runtime.release_jsvalue(keep).unwrap(); + runtime.release_jsvalue(base).unwrap(); } #[test] fn dense_array_read_leaf_keeps_frozen_materialized_values_on_canonical_path() { let runtime = Runtime::new(); let mut context = runtime.new_context(); - let base = context - .eval("globalThis.frozenRead = Object.freeze([7, -0])") + let base = runtime + .into_jsvalue( + context + .eval("globalThis.frozenRead = Object.freeze([7, -0])") + .unwrap(), + ) .unwrap(); runtime.run_gc().unwrap(); assert!(runtime.try_dense_array_immediate_read(&base, 0).is_none()); @@ -1249,13 +1343,14 @@ mod dense_array_read_tests { .unwrap(), Value::Bool(true) ); + runtime.release_jsvalue(base).unwrap(); } #[test] fn dense_array_read_leaf_does_not_drain_deferred_or_borrowed_state() { let runtime = Runtime::new(); - let base = receiver(&runtime, "[1]"); - let _keep = base.clone(); + let base = runtime.into_jsvalue(receiver(&runtime, "[1]")).unwrap(); + let keep = runtime.dup_jsvalue(&base).unwrap(); { let _borrow = runtime.0.state.borrow(); assert!(runtime.try_dense_array_immediate_read(&base, 0).is_none()); @@ -1273,8 +1368,10 @@ mod dense_array_read_tests { runtime.run_gc().unwrap(); assert!(matches!( runtime.try_dense_array_immediate_read(&base, 0), - Some(Value::Int(1)) + Some(JsValue::Int(1)) )); + runtime.release_jsvalue(keep).unwrap(); + runtime.release_jsvalue(base).unwrap(); } } @@ -1282,6 +1379,7 @@ mod dense_array_read_tests { mod ordinary_field_leaf_tests { use super::*; use crate::engine::code::{bytecode::Instruction, runtime::PublishedFunctionSnapshot}; + use crate::engine::object::OrdinaryRead; fn executable(runtime: &Runtime, field: &str) -> (PublishedFunctionSnapshot, u32) { let mut context = runtime.new_context(); @@ -1309,6 +1407,12 @@ mod ordinary_field_leaf_tests { (executable, index) } + fn release_read(runtime: &Runtime, read: Option) { + if let Some(OrdinaryRead::Complete(Some(value))) = read { + runtime.release_jsvalue(value).unwrap(); + } + } + #[test] fn ordinary_field_leaf_preserves_scalar_values_flags_and_declines_nonlocal_rules() { let runtime = Runtime::new(); @@ -1322,8 +1426,8 @@ mod ordinary_field_leaf_tests { "({x:1.5})", "({x:-0})", ] { - let base = context.eval(source).unwrap(); - let _retained = base.clone(); + let base = runtime.into_jsvalue(context.eval(source).unwrap()).unwrap(); + let retained = runtime.dup_jsvalue(&base).unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&base, &executable, index) @@ -1333,12 +1437,14 @@ mod ordinary_field_leaf_tests { &base, &executable, index, - &Value::Int(17) + &JsValue::Int(17) )); assert_eq!( runtime.try_ordinary_field_immediate_read(&base, &executable, index), - Some(Value::Int(17)) + Some(JsValue::Int(17)) ); + runtime.release_jsvalue(retained).unwrap(); + runtime.release_jsvalue(base).unwrap(); } for source in [ "({get x(){throw 42}})", @@ -1352,8 +1458,8 @@ mod ordinary_field_leaf_tests { "new Uint8Array(1)", "globalThis", ] { - let base = context.eval(source).unwrap(); - let _retained = base.clone(); + let base = runtime.into_jsvalue(context.eval(source).unwrap()).unwrap(); + let retained = runtime.dup_jsvalue(&base).unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&base, &executable, index) @@ -1365,31 +1471,35 @@ mod ordinary_field_leaf_tests { &base, &executable, index, - &Value::Int(17) + &JsValue::Int(17) ), "{source}" ); + runtime.release_jsvalue(retained).unwrap(); + runtime.release_jsvalue(base).unwrap(); } for source in [ "Object.freeze({x:42})", "Object.defineProperty({},'x',{value:42,writable:false})", ] { - let base = context.eval(source).unwrap(); - let _retained = base.clone(); + let base = runtime.into_jsvalue(context.eval(source).unwrap()).unwrap(); + let retained = runtime.dup_jsvalue(&base).unwrap(); assert_eq!( runtime.try_ordinary_field_immediate_read(&base, &executable, index), - Some(Value::Int(42)) + Some(JsValue::Int(42)) ); assert!(!runtime.try_ordinary_field_immediate_write( &base, &executable, index, - &Value::Int(17) + &JsValue::Int(17) )); assert_eq!( runtime.try_ordinary_field_immediate_read(&base, &executable, index), - Some(Value::Int(42)) + Some(JsValue::Int(42)) ); + runtime.release_jsvalue(retained).unwrap(); + runtime.release_jsvalue(base).unwrap(); } } @@ -1400,14 +1510,21 @@ mod ordinary_field_leaf_tests { let mut context = runtime.new_context(); let (code, index) = executable(&runtime, "x"); let (foreign_code, foreign_index) = executable(&foreign, "x"); - let base = context.eval("({x:42})").unwrap(); + let base = runtime + .into_jsvalue(context.eval("({x:42})").unwrap()) + .unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&base, &code, index) .is_none() ); - assert!(!runtime.try_ordinary_field_immediate_write(&base, &code, index, &Value::Int(17))); - let _retained = base.clone(); + assert!(!runtime.try_ordinary_field_immediate_write( + &base, + &code, + index, + &JsValue::Int(17) + )); + let retained = runtime.dup_jsvalue(&base).unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&base, &foreign_code, foreign_index) @@ -1417,7 +1534,7 @@ mod ordinary_field_leaf_tests { &base, &foreign_code, foreign_index, - &Value::Int(17) + &JsValue::Int(17) )); assert!( runtime @@ -1442,7 +1559,7 @@ mod ordinary_field_leaf_tests { &base, &code, index, - &Value::Int(17) + &JsValue::Int(17) )); drop(queued); } @@ -1451,21 +1568,28 @@ mod ordinary_field_leaf_tests { .try_ordinary_field_immediate_read(&base, &code, index) .is_none() ); - assert!(!runtime.try_ordinary_field_immediate_write(&base, &code, index, &Value::Int(17))); + assert!(!runtime.try_ordinary_field_immediate_write( + &base, + &code, + index, + &JsValue::Int(17) + )); assert!(runtime.0.deferred_references.has_pending()); runtime.drain_deferred_references().unwrap(); assert_eq!( runtime.try_ordinary_field_immediate_read(&base, &code, index), - Some(Value::Int(42)) + Some(JsValue::Int(42)) ); - let object_value = Value::Object(runtime.new_object(None).unwrap()); + let object_value = runtime + .into_jsvalue(Value::Object(runtime.new_object(None).unwrap())) + .unwrap(); assert!(!runtime.try_ordinary_field_immediate_write(&base, &code, index, &object_value)); assert_eq!( runtime.try_ordinary_field_immediate_read(&base, &code, index), - Some(Value::Int(42)) + Some(JsValue::Int(42)) ); let (lazy_code, lazy_index) = executable(&runtime, "min"); - let math = context.eval("Math").unwrap(); + let math = runtime.into_jsvalue(context.eval("Math").unwrap()).unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&math, &lazy_code, lazy_index) @@ -1475,12 +1599,16 @@ mod ordinary_field_leaf_tests { &math, &lazy_code, lazy_index, - &Value::Int(17) + &JsValue::Int(17) )); assert!(matches!( context.eval("typeof Math.min").unwrap(), Value::String(_) )); + runtime.release_jsvalue(retained).unwrap(); + runtime.release_jsvalue(base).unwrap(); + runtime.release_jsvalue(object_value).unwrap(); + runtime.release_jsvalue(math).unwrap(); } #[test] fn recovery_length_leaf_preserves_utf16_brand_and_final_owner_boundaries() { @@ -1488,62 +1616,74 @@ mod ordinary_field_leaf_tests { let mut context = runtime.new_context(); let (code, index) = executable(&runtime, "length"); for (source, expected) in [("[1,,3]", 3), ("'a\\ud83d\\ude00'", 3)] { - let base = context.eval(source).unwrap(); - let retained = base.clone(); - assert_eq!( - runtime.try_ordinary_field_immediate_read(&base, &code, index), - Some(Value::Int(expected)) - ); - drop(retained); + let base = runtime.into_jsvalue(context.eval(source).unwrap()).unwrap(); + let retained = runtime.dup_jsvalue(&base).unwrap(); + let actual = runtime + .try_ordinary_field_immediate_read(&base, &code, index) + .unwrap(); + assert_eq!(runtime.root_value(&actual).unwrap(), Value::Int(expected)); + runtime.release_jsvalue(retained).unwrap(); + runtime.release_jsvalue(base).unwrap(); } - let proxy = context.eval("new Proxy([], {get(){throw 91}})").unwrap(); - let _retained = proxy.clone(); + let proxy = runtime + .into_jsvalue(context.eval("new Proxy([], {get(){throw 91}})").unwrap()) + .unwrap(); + let proxy_retained = runtime.dup_jsvalue(&proxy).unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&proxy, &code, index) .is_none() ); - let unique = Value::String(crate::engine::value::JsString::from_owned_utf16(vec![ - 97, 0xd800, - ])); + let unique = runtime + .into_jsvalue(Value::String( + crate::engine::value::JsString::from_owned_utf16(vec![97, 0xd800]), + )) + .unwrap(); assert!( runtime .try_ordinary_field_immediate_read(&unique, &code, index) .is_none() ); - let retained = unique.clone(); - assert_eq!( - runtime.try_ordinary_field_immediate_read(&unique, &code, index), - Some(Value::Int(2)) - ); - drop(retained); + let unique_retained = runtime.dup_jsvalue(&unique).unwrap(); + let actual = runtime + .try_ordinary_field_immediate_read(&unique, &code, index) + .unwrap(); + assert_eq!(runtime.root_value(&actual).unwrap(), Value::Int(2)); + runtime.release_jsvalue(proxy_retained).unwrap(); + runtime.release_jsvalue(proxy).unwrap(); + runtime.release_jsvalue(unique_retained).unwrap(); + runtime.release_jsvalue(unique).unwrap(); } #[test] fn recovery_arguments_leaf_reads_current_cell_and_declines_redefinitions() { let runtime = Runtime::new(); let mut context = runtime.new_context(); - let base = context - .eval( - "globalThis.args=(function(a){globalThis.change=x=>a=x;return arguments})(1);args", + let base = runtime + .into_jsvalue( + context + .eval( + "globalThis.args=(function(a){globalThis.change=x=>a=x;return arguments})(1);args", + ) + .unwrap(), ) .unwrap(); - let _retained = base.clone(); + let retained = runtime.dup_jsvalue(&base).unwrap(); assert_eq!( runtime.try_array_immediate_read(&base, 0), - Some(Value::Int(1)) + Some(JsValue::Int(1)) ); context.eval("change(7)").unwrap(); assert_eq!( runtime.try_array_immediate_read(&base, 0), - Some(Value::Int(7)) + Some(JsValue::Int(7)) ); context .eval("Object.defineProperty(args,'0',{value:8,writable:false});change(9)") .unwrap(); assert_eq!( runtime.try_array_immediate_read(&base, 0), - Some(Value::Int(8)) + Some(JsValue::Int(8)) ); context .eval("Object.defineProperty(args,'0',{get(){return 11},configurable:true})") @@ -1552,44 +1692,59 @@ mod ordinary_field_leaf_tests { assert_eq!(context.eval("args[0]").unwrap(), Value::Int(11)); context.eval("delete args[0]").unwrap(); assert!(runtime.try_array_immediate_read(&base, 0).is_none()); + runtime.release_jsvalue(retained).unwrap(); + runtime.release_jsvalue(base).unwrap(); } #[test] fn linked_native_fact_is_bound_to_the_selected_callee_not_a_property_cache() { use crate::engine::builtins::native::{MathMinMaxKind, NativeFunctionId}; - use crate::engine::object::OrdinaryRead; let runtime = Runtime::new(); let mut context = runtime.new_context(); let (code, index) = executable(&runtime, "x"); - let base = context - .eval("globalThis.selectedNative={x:Math.min};selectedNative") + let base = runtime + .into_jsvalue( + context + .eval("globalThis.selectedNative={x:Math.min};selectedNative") + .unwrap(), + ) .unwrap(); let mut fact = None; - let Some(OrdinaryRead::Complete(Some(Value::Object(callee)))) = runtime + let Some(OrdinaryRead::Complete(Some(value))) = runtime .prepare_linked_own_read_selected(&base, &code, index, Some(&mut fact)) .unwrap() else { panic!("own native") }; + let Value::Object(callee) = runtime.root_and_release_jsvalue(value).unwrap() else { + panic!("own native") + }; context.eval("selectedNative.x=Math.max").unwrap(); let data = fact.take().unwrap().into_parts(&callee).unwrap(); assert_eq!( data.target, NativeFunctionId::MathMinMax(MathMinMaxKind::Min) ); - let _new_read = runtime - .prepare_linked_own_read_selected(&base, &code, index, Some(&mut fact)) - .unwrap(); + release_read( + &runtime, + runtime + .prepare_linked_own_read_selected(&base, &code, index, Some(&mut fact)) + .unwrap(), + ); assert!(fact.take().unwrap().into_parts(&callee).is_none()); - let _new_read = runtime - .prepare_linked_own_read_selected(&base, &code, index, Some(&mut fact)) - .unwrap(); + release_read( + &runtime, + runtime + .prepare_linked_own_read_selected(&base, &code, index, Some(&mut fact)) + .unwrap(), + ); let foreign = Runtime::new(); let mut foreign_context = foreign.new_context(); let Value::Object(foreign_callee) = foreign_context.eval("Math.max").unwrap() else { panic!("native") }; assert!(fact.take().unwrap().into_parts(&foreign_callee).is_none()); + runtime.release_jsvalue(base).unwrap(); } #[test] diff --git a/src/engine/object/ordinary_storage/ic.rs b/src/engine/object/ordinary_storage/ic.rs index 1f5d72c6..c59d6e63 100644 --- a/src/engine/object/ordinary_storage/ic.rs +++ b/src/engine/object/ordinary_storage/ic.rs @@ -1,9 +1,10 @@ //! Promote a location-cache hit without draining runtime cleanup or invoking JS. use super::{LinkedNativeSelection, linked_field_atom}; use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; +use crate::engine::atom::AtomIdx; use crate::engine::code::runtime::PublishedFunctionSnapshot; use crate::engine::heap::{ObjectPayload, RawValue, SlotReleaseReadiness}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; impl Runtime { /// A miss only records a location and leaves the canonical read untouched. @@ -11,13 +12,13 @@ impl Runtime { /// it never caches a value or outlives the result's ordinary slot owner. pub(crate) fn try_property_ic_read_owned( &self, - base: &Value, + base: &JsValue, executable: &PublishedFunctionSnapshot, pc: usize, key_index: u32, keep_receiver: bool, native: &mut Option, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { let Some(atom) = linked_field_atom(self, executable, key_index) else { return Ok(None); }; @@ -34,8 +35,7 @@ impl Runtime { return Ok(None); } let receiver = match base { - Value::Object(object) if object.belongs_to(self) => object.object_id(), - Value::Object(_) => return Ok(None), + JsValue::Object(object) => *object, _ => { cache.miss( &state.heap, @@ -90,12 +90,13 @@ impl Runtime { } else { None }; - // String/BigInt clone their backing owner; Object/Symbol retain checks - // overflow before changing the count. No public value conversion below - // can fail after the sentinel exclusion above. + // Every heap-backed kind retains one new edge; the internal-value + // conversion below cannot fail after the sentinel exclusion above. state.retain_raw_root(&raw)?; drop(state); - let value = self.take_owned_raw_value(raw)?; + let value = JsValue::from_raw(raw).ok_or(RuntimeError::Invariant( + "internal value sentinel occupied a cached property slot", + ))?; *native = selected.map(|(function, data)| LinkedNativeSelection { runtime: self.clone(), function, @@ -105,32 +106,134 @@ impl Runtime { crate::engine::api::profiling::record_owned_execution_event("property_ic.hit"); Ok(Some(value)) } + + /// Trusted shared-borrow data-property read. + /// + /// Covers the location-cache hit for a live receiver without a mutable + /// state borrow or fallible plumbing. Symbols need an atom-table retain + /// (S1b) and every non-data or non-cached case declines with `None`, so the + /// caller keeps its canonical `try_property_ic_read_owned` fallback. A + /// declined read claims no owner. + #[inline] + pub(crate) fn property_ic_read_fast( + &self, + base: &JsValue, + executable: &PublishedFunctionSnapshot, + pc: usize, + key_index: u32, + keep_receiver: bool, + native: &mut Option, + ) -> Option { + let atom = linked_field_atom(self, executable, key_index)?; + let cache = executable.property_read_ic.site(pc)?; + if !keep_receiver && self.0.deferred_references.has_pending() { + return None; + } + let state = self.0.state.try_borrow().ok()?; + if !keep_receiver && state.heap.has_pending_zero_cleanup() { + return None; + } + let receiver = match base { + JsValue::Object(object) => *object, + _ => { + cache.miss( + &state.heap, + &state.atoms, + self.domain_id(), + executable.realm, + None, + atom, + ); + return None; + } + }; + if !keep_receiver + && state.heap.slot_object_release_readiness_fast(receiver) + != SlotReleaseReadiness::Ready + { + return None; + } + let Some(raw) = cache.read(&state.heap, self.domain_id(), executable.realm, receiver) + else { + cache.miss( + &state.heap, + &state.atoms, + self.domain_id(), + executable.realm, + Some(receiver), + atom, + ); + return None; + }; + match raw { + RawValue::Object(function) => { + let selected = if keep_receiver { + let object = state.heap.object_fast(*function); + match &object.payload { + ObjectPayload::NativeFunction { data, .. } => { + data.realm.and_then(|realm| { + (data.operation().is_some() && state.heap.context(realm).is_ok()) + .then_some((*function, *data)) + }) + } + _ => None, + } + } else { + None + }; + state.heap.retain_object_fast(*function); + *native = selected.map(|(function, data)| LinkedNativeSelection { + runtime: self.clone(), + function, + data, + }); + Some(JsValue::Object(*function)) + } + RawValue::String(id) => { + state.heap.retain_string_shared(*id).ok()?; + Some(JsValue::String(*id)) + } + RawValue::BigInt(id) => { + state.heap.retain_bigint_shared(*id).ok()?; + Some(JsValue::BigInt(*id)) + } + RawValue::Symbol(index) => { + state.atoms.retain_index_shared(*index).ok()?; + Some(JsValue::Symbol(*index)) + } + RawValue::Undefined => Some(JsValue::Undefined), + RawValue::Null => Some(JsValue::Null), + RawValue::Bool(value) => Some(JsValue::Bool(*value)), + RawValue::Int(value) => Some(JsValue::Int(*value)), + RawValue::Float(value) => Some(JsValue::Float(*value)), + RawValue::Private(_) | RawValue::Uninitialized | RawValue::Exception => None, + } + } } impl Runtime { pub(crate) fn try_property_ic_write_owned( &self, - base: &Value, + base: &JsValue, executable: &PublishedFunctionSnapshot, pc: usize, key: u32, - value: &Value, + value: &JsValue, ) -> Result { let Some(atom) = linked_field_atom(self, executable, key) else { return Ok(false); }; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return Ok(false); }; - if !object.belongs_to(self) { - return Ok(false); - } let Some(cache) = executable.property_read_ic.write_site(pc) else { return Ok(false); }; - let raw = self.raw_property_value(value)?; + // The borrowed value already carries its edges; the stored copy is + // retained transactionally below, so no producer edge is created. + let raw = value.as_raw(); let mut state = self.0.state.borrow_mut(); - let id = object.object_id(); + let id = *object; let slot = match cache.slot(&state.heap, self.domain_id(), executable.realm, id) { Some(slot) => slot, None => { @@ -144,6 +247,7 @@ impl Runtime { ); let Some(slot) = cache.slot(&state.heap, self.domain_id(), executable.realm, id) else { + drop(state); return Ok(false); }; slot @@ -151,7 +255,10 @@ impl Runtime { }; // Input owners remain rooted; retain the new value before releasing the // old edge. The caller has ended RunSlots and published the current PC. - state.replace_property_slot(id, slot, crate::engine::heap::PropertySlot::Data(raw))?; + let replaced = + state.replace_property_slot(id, slot, crate::engine::heap::PropertySlot::Data(raw)); + drop(state); + replaced?; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event("property_write_ic.hit"); Ok(true) @@ -159,32 +266,44 @@ impl Runtime { pub(crate) fn try_dense_array_write_owned( &self, - base: &Value, + base: &JsValue, index: u32, - value: &Value, + value: &JsValue, ) -> Result { - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return Ok(false); }; - if !object.belongs_to(self) { - return Ok(false); - } - let raw = self.raw_property_value(value)?; + // The borrowed value already carries its edges; the stored copy is + // retained transactionally below, so no producer edge is created. + let raw = value.as_raw(); let mut state = self.0.state.borrow_mut(); - let data = state.heap.object(object.object_id())?; - if data.kind != crate::engine::heap::ObjectKind::Array + let data = match state.heap.object(*object) { + Ok(data) => data, + Err(error) => { + drop(state); + return Err(error.into()); + } + }; + if !matches!(data.kind, crate::engine::heap::ObjectKind::Array) || data.dense_array_value(index).is_none() { return Ok(false); } - let atoms = state.retain_raw_value_atoms([&raw])?; - match state - .heap - .replace_array_dense_value(object.object_id(), index, raw) - { - Ok(cleanup) => state.apply_cleanup(cleanup)?, + let atoms = match state.retain_raw_value_atoms([&raw]) { + Ok(atoms) => atoms, Err(error) => { - state.release_atoms(atoms)?; + drop(state); + return Err(error); + } + }; + let appended = state.heap.replace_array_dense_value(*object, index, raw); + match appended { + Ok(cleanup) => { + state.apply_cleanup(cleanup)?; + } + Err(error) => { + let released = state.release_atoms(atoms); + released?; return Err(error.into()); } } @@ -193,31 +312,37 @@ impl Runtime { pub(crate) fn try_define_field_owned( &self, - base: &Value, + base: &JsValue, executable: &PublishedFunctionSnapshot, key: u32, - value: &Value, + value: &JsValue, ) -> Result { let Some(atom) = linked_field_atom(self, executable, key) else { return Ok(false); }; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return Ok(false); }; - if !object.belongs_to(self) { - return Ok(false); - } - let raw = self.raw_property_value(value)?; + // The borrowed value already carries its edges; the stored copy is + // retained transactionally below, so no producer edge is created. + let raw = value.as_raw(); let mut state = self.0.state.borrow_mut(); - let data = state.heap.object(object.object_id())?; - if !super::is_ordinary(data) - || !data.extensible - || state.heap.shape(data.shape)?.find(atom).is_some() - { + let data = match state.heap.object(*object) { + Ok(data) => data, + Err(error) => { + drop(state); + return Err(error.into()); + } + }; + let shape_has_atom = state + .heap + .shape(data.shape) + .map(|shape| shape.find(AtomIdx::from_raw(atom.raw())).is_some())?; + if !super::is_ordinary(data) || !data.extensible || shape_has_atom { return Ok(false); } state.store_selected_property_slot( - object.object_id(), + *object, atom, crate::engine::object::shape::PropertyFlags::data(true, true, true), crate::engine::heap::PropertySlot::Data(raw), @@ -228,23 +353,20 @@ impl Runtime { pub(crate) fn try_delete_own_data( &self, - base: &Value, + base: &JsValue, key: &crate::engine::object::PropertyKey, ) -> Result, RuntimeError> { - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return Ok(None); }; - if !object.belongs_to(self) { - return Ok(None); - } { let state = self.0.state.borrow(); - let data = state.heap.object(object.object_id())?; + let data = state.heap.object(*object)?; if !super::is_ordinary(data) { return Ok(None); } let shape = state.heap.shape(data.shape)?; - let Some(slot) = shape.find(key.atom()) else { + let Some(slot) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(Some(true)); }; if !shape.entries()[slot as usize].flags.configurable @@ -256,57 +378,56 @@ impl Runtime { return Ok(None); } } - self.delete_property(object, key).map(Some) + let object = crate::engine::object::ObjectRef::from_borrowed_handle(self.clone(), *object)?; + self.delete_property(&object, key).map(Some) } } impl Runtime { - pub(crate) fn try_dense_array_kept_read(&self, base: &Value, index: u32) -> Option { - let Value::Object(object) = base else { + pub(crate) fn try_dense_array_kept_read(&self, base: &JsValue, index: u32) -> Option { + let JsValue::Object(object) = base else { return None; }; - if !object.belongs_to(self) { - return None; - } let state = self.0.state.borrow(); - let data = state.heap.object(object.object_id()).ok()?; - if data.kind != crate::engine::heap::ObjectKind::Array { + let data = state.heap.object(*object).ok()?; + if !matches!(data.kind, crate::engine::heap::ObjectKind::Array) { return None; } - super::immediate_value(data.dense_array_value(index)?) + super::immediate_value_jsvalue(data.dense_array_value(index)?) } } impl Runtime { pub(crate) fn try_property_ic_write_scalar( &self, - base: &Value, + base: &JsValue, executable: &PublishedFunctionSnapshot, pc: usize, key: u32, - value: &Value, + value: &JsValue, ) -> Result { if !matches!( value, - Value::Undefined | Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) - ) || self.slot_value_release_readiness(base)? != SlotReleaseReadiness::Ready + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) + ) || self.slot_value_release_readiness_jsvalue(base)? != SlotReleaseReadiness::Ready { return Ok(false); } let Some(atom) = linked_field_atom(self, executable, key) else { return Ok(false); }; - let Value::Object(object) = base else { + let JsValue::Object(object) = base else { return Ok(false); }; - if !object.belongs_to(self) { - return Ok(false); - } let Some(cache) = executable.property_read_ic.write_site(pc) else { return Ok(false); }; let mut state = self.0.state.borrow_mut(); - let id = object.object_id(); + let id = *object; let slot = match cache.slot(&state.heap, self.domain_id(), executable.realm, id) { Some(slot) => slot, None => { @@ -332,7 +453,9 @@ impl Runtime { if super::immediate_value(old).is_none() { return Ok(false); } - let raw = self.raw_property_value(value)?; + // `value` was matched to a scalar above, so this id copy allocates + // nothing and never takes the state borrow the caller still holds. + let raw = value.as_raw(); state.replace_property_slot(id, slot, crate::engine::heap::PropertySlot::Data(raw))?; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event("property_write_ic.hit"); @@ -343,14 +466,18 @@ impl Runtime { impl Runtime { pub(crate) fn try_dense_array_write_scalar( &self, - base: &Value, + base: &JsValue, index: u32, - value: &Value, + value: &JsValue, ) -> Result { if !matches!( value, - Value::Undefined | Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) - ) || self.slot_value_release_readiness(base)? != SlotReleaseReadiness::Ready + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) + ) || self.slot_value_release_readiness_jsvalue(base)? != SlotReleaseReadiness::Ready { return Ok(false); } @@ -365,12 +492,13 @@ impl Runtime { mod tests { use super::*; use crate::engine::code::bytecode::Instruction; + use crate::engine::value::Value; - fn object(value: &Value) -> &crate::engine::object::ObjectRef { - let Value::Object(object) = value else { + fn object(value: &JsValue) -> crate::engine::heap::ObjectId { + let JsValue::Object(object) = value else { panic!("object") }; - object + *object } fn site(runtime: &Runtime) -> (PublishedFunctionSnapshot, usize, u32) { @@ -412,7 +540,15 @@ mod tests { "({nested:7})", ] { let (code, pc, key) = site(&runtime); - let base=context.eval(&format!("globalThis.icExpected={expression};globalThis.icHolder={{x:icExpected}};icHolder")).unwrap(); + let base = runtime + .into_jsvalue( + context + .eval(&format!( + "globalThis.icExpected={expression};globalThis.icHolder={{x:icExpected}};icHolder" + )) + .unwrap(), + ) + .unwrap(); let expected = context.eval("icExpected").unwrap(); let mut native = None; assert!( @@ -425,15 +561,21 @@ mod tests { .try_property_ic_read_owned(&base, &code, pc, key, false, &mut native) .unwrap() .unwrap(); - assert_eq!(actual, expected, "{expression}"); + assert_eq!( + runtime.root_and_release_jsvalue(actual).unwrap(), + expected, + "{expression}" + ); context.eval("icHolder.x=99").unwrap(); + let after = runtime + .try_property_ic_read_owned(&base, &code, pc, key, false, &mut native) + .unwrap(); assert_eq!( - runtime - .try_property_ic_read_owned(&base, &code, pc, key, false, &mut native) - .unwrap(), + after.map(|value| runtime.root_and_release_jsvalue(value).unwrap()), Some(Value::Int(99)) ); assert!(native.is_none()); + runtime.release_jsvalue(base).unwrap(); } } @@ -442,7 +584,9 @@ mod tests { let runtime = Runtime::new(); let mut context = runtime.new_context(); let (code, pc, key) = site(&runtime); - let base = context.eval("({x:{marker:1}})").unwrap(); + let base = runtime + .into_jsvalue(context.eval("({x:{marker:1}})").unwrap()) + .unwrap(); let mut native = None; assert!( runtime @@ -464,7 +608,7 @@ mod tests { .state .borrow() .heap - .object_strong_count(receiver.object_id()) + .object_strong_count(receiver) .unwrap(), 1 ); @@ -485,18 +629,23 @@ mod tests { assert!(runtime.0.deferred_references.has_pending()); // A kept receiver hit only retains under the exclusive heap borrow; // pending unrelated releases cannot mutate its guarded layout. - let retained_hit = runtime + let first_hit = runtime .try_property_ic_read_owned(&base, &code, pc, key, true, &mut native) .unwrap(); - assert!(matches!(retained_hit, Some(Value::Object(_)))); + assert!(matches!(first_hit, Some(JsValue::Object(_)))); assert!(runtime.0.deferred_references.has_pending()); runtime.drain_deferred_references().unwrap(); - assert!(matches!( - runtime - .try_property_ic_read_owned(&base, &code, pc, key, true, &mut native) - .unwrap(), - Some(Value::Object(_)) - )); + let second_hit = runtime + .try_property_ic_read_owned(&base, &code, pc, key, true, &mut native) + .unwrap(); + assert!(matches!(second_hit, Some(JsValue::Object(_)))); + if let Some(value) = first_hit { + runtime.release_jsvalue(value).unwrap(); + } + if let Some(value) = second_hit { + runtime.release_jsvalue(value).unwrap(); + } + runtime.release_jsvalue(base).unwrap(); } #[test] @@ -504,8 +653,12 @@ mod tests { let runtime = Runtime::new(); let mut context = runtime.new_context(); let (code, pc, key) = site(&runtime); - let base = context - .eval("globalThis.icNative={x:Math.min};icNative") + let base = runtime + .into_jsvalue( + context + .eval("globalThis.icNative={x:Math.min};icNative") + .unwrap(), + ) .unwrap(); let mut native = None; assert!( @@ -518,9 +671,12 @@ mod tests { .try_property_ic_read_owned(&base, &code, pc, key, true, &mut native) .unwrap() .unwrap(); + let first_object = + crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), object(&first)) + .unwrap(); let hint = native.take().unwrap(); context.eval("icNative.x=Math.max").unwrap(); - let data = hint.into_parts(object(&first)).unwrap(); + let data = hint.into_parts(&first_object).unwrap(); assert_eq!( data.target, crate::engine::builtins::native::NativeFunctionId::MathMinMax( @@ -532,7 +688,10 @@ mod tests { .unwrap() .unwrap(); let hint = native.take().unwrap(); - assert!(hint.into_parts(object(&first)).is_none()); + assert!(hint.into_parts(&first_object).is_none()); assert_ne!(first, second); + runtime.release_jsvalue(first).unwrap(); + runtime.release_jsvalue(second).unwrap(); + runtime.release_jsvalue(base).unwrap(); } } diff --git a/src/engine/object/private_elements.rs b/src/engine/object/private_elements.rs index aa8967ea..dc73db92 100644 --- a/src/engine/object/private_elements.rs +++ b/src/engine/object/private_elements.rs @@ -11,7 +11,7 @@ use crate::engine::api::error::{Error, ErrorKind, NativeErrorMessage}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::{Atom, AtomKind}; +use crate::engine::atom::{Atom, AtomIdx, AtomKind}; use crate::engine::code::function::metadata::{ ClosureVariableKind, ConstructorKind, EvalKind, FunctionKind, }; @@ -54,14 +54,30 @@ impl Runtime { self.validate_private_receiver(receiver, name)?; self.validate_value_domain(&value, "private field value")?; let raw = self.raw_property_value(&value)?; + // Clone duplicates only the handle; the probe keeps the producer edge + // accountable through every store-or-decline path below. + let conversion_probe = raw.clone(); let object_id = receiver.object_id(); let duplicate = { let state = self.0.state.borrow(); - let object = state.heap.object(object_id)?; - state.heap.shape(object.shape)?.find(name.atom()).is_some() + let found = state.heap.object(object_id).and_then(|object| { + state + .heap + .shape(object.shape) + .map(|shape| shape.find(AtomIdx::from_raw(name.atom().raw())).is_some()) + }); + match found { + Ok(duplicate) => duplicate, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error.into()); + } + } }; if duplicate { + self.release_converted_value_edge(&conversion_probe); return Err(RuntimeError::Engine(self.private_field_error( name, "private class field '", @@ -71,33 +87,51 @@ impl Runtime { let mut state = self.0.state.borrow_mut(); let (prototype, mut entries, mut slots) = { - let object = state.heap.object(object_id)?; - let shape = state.heap.shape(object.shape)?; + let snapshot = state.heap.object(object_id).and_then(|object| { + state.heap.shape(object.shape).map(|shape| { + ( + shape.prototype(), + shape.entries().to_vec(), + object.slots.clone(), + ) + }) + }); + let (prototype, entries, slots) = match snapshot { + Ok(snapshot) => snapshot, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error.into()); + } + }; // Recheck under the mutable borrow so a future interior mutator // cannot turn the snapshot above into a duplicate transition. - if shape.find(name.atom()).is_some() { + if entries + .iter() + .any(|entry| entry.atom == AtomIdx::from_raw(name.atom().raw())) + { drop(state); + self.release_converted_value_edge(&conversion_probe); return Err(RuntimeError::Engine(self.private_field_error( name, "private class field '", "' already exists", )?)); } - ( - shape.prototype(), - shape.entries().to_vec(), - object.slots.clone(), - ) + (prototype, entries, slots) }; entries.push(ShapeEntry { - atom: name.atom(), + atom: AtomIdx::from_raw(name.atom().raw()), flags: PropertyFlags::data(true, true, true), }); slots.push(PropertySlot::Data(raw)); - state.replace_layout(object_id, prototype, &entries, slots)?; + let layout_result = state.replace_layout(object_id, prototype, &entries, slots); drop(state); - // `replace_layout` retained the heap occurrence before this incoming - // public root is released. + // `replace_layout` retained the heap occurrence on success; a rejected + // layout never stored the value. Balance the producer edge either way + // before this incoming public root is released. + self.release_converted_value_edge(&conversion_probe); + layout_result?; drop(value); Ok(()) } @@ -114,7 +148,7 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(receiver.object_id())?; let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(name.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(name.atom().raw())) else { drop(state); return Err(RuntimeError::Engine(self.private_field_error( name, @@ -156,13 +190,31 @@ impl Runtime { self.validate_private_receiver(receiver, name)?; self.validate_value_domain(&value, "private field value")?; let raw = self.raw_property_value(&value)?; + // Clone duplicates only the handle; the probe keeps the producer edge + // accountable through every store-or-decline path below. + let conversion_probe = raw.clone(); let object_id = receiver.object_id(); let index = { let state = self.0.state.borrow(); - let object = state.heap.object(object_id)?; - let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(name.atom()) else { + let object = match state.heap.object(object_id) { + Ok(object) => object, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error.into()); + } + }; + let shape = match state.heap.shape(object.shape) { + Ok(shape) => shape, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error.into()); + } + }; + let Some(index) = shape.find(AtomIdx::from_raw(name.atom().raw())) else { drop(state); + self.release_converted_value_edge(&conversion_probe); return Err(RuntimeError::Engine(self.private_field_error( name, "private class field '", @@ -172,6 +224,8 @@ impl Runtime { let index = usize::try_from(index) .map_err(|_| RuntimeError::Invariant("private field index does not fit usize"))?; if !matches!(object.slots.get(index), Some(PropertySlot::Data(_))) { + drop(state); + self.release_converted_value_edge(&conversion_probe); return Err(RuntimeError::Invariant( "private data field used non-data storage", )); @@ -181,10 +235,12 @@ impl Runtime { let replacement = PropertySlot::Data(raw); let mut state = self.0.state.borrow_mut(); - state.replace_property_slot(object_id, index, replacement)?; + let replaced = state.replace_property_slot(object_id, index, replacement); drop(state); - // `replace_object_slot` retained the heap occurrence before this - // incoming public root is released. + // The slot retained its own copy edge on success; balance the + // producer edge either way before releasing the public root. + self.release_converted_value_edge(&conversion_probe); + replaced?; drop(value); Ok(()) } @@ -200,7 +256,11 @@ impl Runtime { self.validate_private_receiver(receiver, name)?; let state = self.0.state.borrow(); let object = state.heap.object(receiver.object_id())?; - Ok(state.heap.shape(object.shape)?.find(name.atom()).is_some()) + Ok(state + .heap + .shape(object.shape)? + .find(AtomIdx::from_raw(name.atom().raw())) + .is_some()) } /// Capture a private-name identity in its dedicated immutable lexical @@ -215,7 +275,7 @@ impl Runtime { let mut state = self.0.state.borrow_mut(); state.atoms.retain(atom)?; let data = VarRefData::captured( - RawValue::Private(atom), + RawValue::Private(state.atoms.unbrand(atom)?), true, true, ClosureVariableKind::PrivateField, @@ -262,9 +322,10 @@ impl Runtime { } } state.atoms.retain(atom)?; + let index = state.atoms.unbrand(atom)?; let cleanup = match state .heap - .replace_var_ref_value(root.id(), RawValue::Private(atom)) + .replace_var_ref_value(root.id(), RawValue::Private(index)) { Ok(cleanup) => cleanup, Err(error) => { @@ -300,7 +361,7 @@ impl Runtime { } match &var_ref.value { RawValue::Private(atom) => { - if state.atoms.kind(*atom)? != AtomKind::Private { + if state.atoms.kind(state.atoms.brand(*atom)?)? != AtomKind::Private { return Err(RuntimeError::Invariant( "private-name VarRef contains a non-private atom", )); @@ -319,6 +380,7 @@ impl Runtime { } } }; + let atom = self.0.state.borrow().atoms.brand(atom)?; PrivateNameRef::from_borrowed_atom(self.clone(), atom).map_err(Into::into) } @@ -543,7 +605,11 @@ impl Runtime { let duplicate = { let state = self.0.state.borrow(); let object = state.heap.object(receiver_id)?; - state.heap.shape(object.shape)?.find(brand).is_some() + state + .heap + .shape(object.shape)? + .find(AtomIdx::from_raw(brand.raw())) + .is_some() }; if duplicate { return Err(RuntimeError::Engine(Error::new( @@ -556,7 +622,7 @@ impl Runtime { let (prototype, mut entries, mut slots) = { let object = state.heap.object(receiver_id)?; let shape = state.heap.shape(object.shape)?; - if shape.find(brand).is_some() { + if shape.find(AtomIdx::from_raw(brand.raw())).is_some() { drop(state); return Err(RuntimeError::Engine(Error::new( ErrorKind::Type, @@ -570,7 +636,7 @@ impl Runtime { ) }; entries.push(ShapeEntry { - atom: brand, + atom: AtomIdx::from_raw(brand.raw()), flags: PropertyFlags::data(true, true, true), }); slots.push(PropertySlot::Data(RawValue::Undefined)); @@ -594,7 +660,11 @@ impl Runtime { let brand = self.private_method_brand_atom(method, kind)?; let state = self.0.state.borrow(); let object = state.heap.object(receiver.object_id())?; - Ok(state.heap.shape(object.shape)?.find(brand).is_some()) + Ok(state + .heap + .shape(object.shape)? + .find(AtomIdx::from_raw(brand.raw())) + .is_some()) } /// Validate that the method's HomeObject already owns a class-side brand. @@ -745,6 +815,7 @@ impl Runtime { mod tests { use crate::engine::code::function::metadata::FunctionMetadata; use crate::engine::object::{DescriptorField, OrdinaryPropertyDescriptor}; + use crate::engine::value::JsValue; use super::*; use crate::engine::code::bytecode::Instruction; @@ -1007,7 +1078,7 @@ mod tests { )) )); assert!(matches!( - runtime.write_var_ref(&captured, Value::Int(1)), + runtime.write_var_ref(&captured, JsValue::Int(1)), Err(RuntimeError::Invariant( "ordinary VarRef write reached a private-element binding" )) diff --git a/src/engine/object/properties.rs b/src/engine/object/properties.rs index 61472c20..d0ce52e7 100644 --- a/src/engine/object/properties.rs +++ b/src/engine/object/properties.rs @@ -3,7 +3,7 @@ use crate::engine::api::error::{Error, ErrorKind, NativeErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::builtins::CanonicalNumericIndex; use crate::engine::code::function::metadata::ClosureVariableKind; use crate::engine::heap::roots::VarRefRoot; @@ -267,7 +267,7 @@ impl Runtime { let state = self.0.state.borrow(); let object_data = state.heap.object(object.object_id())?; let shape = state.heap.shape(object_data.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(None); }; let index = usize::try_from(index) @@ -384,7 +384,7 @@ impl Runtime { let shape = state.heap.shape(object.shape)?; let slot_index = usize::try_from( shape - .find(key.atom()) + .find(AtomIdx::from_raw(key.atom().raw())) .ok_or(RuntimeError::Invariant("autoinit property disappeared"))?, ) .map_err(|_| RuntimeError::Invariant("shape index does not fit usize"))?; @@ -486,9 +486,16 @@ impl Runtime { } }; let raw = self.raw_property_value(&initialized)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); let mut state = self.0.state.borrow_mut(); - state.replace_property_slot(object_id, slot_index, PropertySlot::Data(raw))?; + let replaced = state.replace_property_slot(object_id, slot_index, PropertySlot::Data(raw)); drop(state); + // The slot retained its own copy edge on success; a rejected + // replacement kept nothing. Balance the producer edge either way. + self.release_converted_value_edge(&conversion_probe); + replaced?; drop(initialized); Ok(()) } @@ -790,7 +797,11 @@ impl Runtime { }; let shape = state.heap.shape(object_data.shape)?; for entry in shape.entries() { - if state.atoms.array_index(entry.atom)?.is_some() { + if state + .atoms + .array_index(state.atoms.brand(entry.atom)?)? + .is_some() + { return Err(RuntimeError::Invariant( "fast Array shape already contained a numeric property", )); @@ -819,7 +830,7 @@ impl Runtime { .map_err(|_| RuntimeError::Invariant("fast Array count exceeded Uint32"))?; let key = self.property_key_for_index(index as u64)?; entries.push(ShapeEntry { - atom: key.atom(), + atom: AtomIdx::from_raw(key.atom().raw()), flags: PropertyFlags::data(true, true, true), }); keys.push(key); @@ -837,12 +848,30 @@ impl Runtime { value: &Value, ) -> Result<(), RuntimeError> { let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); let mut state = self.0.state.borrow_mut(); - let retained_atoms = state.retain_raw_value_atoms(std::iter::once(&raw))?; - match state.heap.append_array_dense_value(object.object_id(), raw) { - Ok(()) => Ok(()), + let retained_atoms = match state.retain_raw_value_atoms(std::iter::once(&raw)) { + Ok(atoms) => atoms, Err(error) => { - state.release_atoms(retained_atoms)?; + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } + }; + let appended = state.heap.append_array_dense_value(object.object_id(), raw); + match appended { + Ok(()) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + Ok(()) + } + Err(error) => { + let released = state.release_atoms(retained_atoms); + drop(state); + self.release_converted_value_edge(&conversion_probe); + released?; Err(error.into()) } } @@ -855,15 +884,33 @@ impl Runtime { value: &Value, ) -> Result<(), RuntimeError> { let raw = self.raw_property_value(value)?; + // Clone duplicates only the handle; the probe keeps the + // producer edge accountable through every store-or-decline path. + let conversion_probe = raw.clone(); let mut state = self.0.state.borrow_mut(); - let retained_atoms = state.retain_raw_value_atoms(std::iter::once(&raw))?; - match state + let retained_atoms = match state.retain_raw_value_atoms(std::iter::once(&raw)) { + Ok(atoms) => atoms, + Err(error) => { + drop(state); + self.release_converted_value_edge(&conversion_probe); + return Err(error); + } + }; + let replaced = state .heap - .replace_array_dense_value(object.object_id(), index, raw) - { - Ok(cleanup) => state.apply_cleanup(cleanup), + .replace_array_dense_value(object.object_id(), index, raw); + match replaced { + Ok(cleanup) => { + let applied = state.apply_cleanup(cleanup); + drop(state); + self.release_converted_value_edge(&conversion_probe); + applied + } Err(error) => { - state.release_atoms(retained_atoms)?; + let released = state.release_atoms(retained_atoms); + drop(state); + self.release_converted_value_edge(&conversion_probe); + released?; Err(error.into()) } } @@ -895,7 +942,7 @@ impl Runtime { } let shape = heap.shape(object_data.shape)?; let index = shape - .find(length) + .find(AtomIdx::from_raw(length.raw())) .ok_or(RuntimeError::Invariant("Array has no length property"))?; if index != 0 { return Err(RuntimeError::Invariant( @@ -1320,6 +1367,7 @@ impl Runtime { step = match step { crate::engine::object::ArrayLengthStep::Complete(result) => return Ok(result), crate::engine::object::ArrayLengthStep::Number { value, resume } => { + let value = self.root_and_release_jsvalue(value)?; resume.number(self, self.array_length_to_number(realm, &value)?)? } }; @@ -1390,7 +1438,7 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(object.object_id())?; let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(None); }; let index = usize::try_from(index) @@ -1438,7 +1486,11 @@ impl Runtime { } let state = self.0.state.borrow(); let object = state.heap.object(object.object_id())?; - Ok(state.heap.shape(object.shape)?.find(key.atom()).is_some()) + Ok(state + .heap + .shape(object.shape)? + .find(AtomIdx::from_raw(key.atom().raw())) + .is_some()) } /// Read an own property's enumerable bit without materializing autoinit @@ -1472,7 +1524,7 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(object.object_id())?; let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(false); }; let index = usize::try_from(index) @@ -1524,7 +1576,7 @@ impl Runtime { match &object_data.payload { ObjectPayload::GlobalObject { uninitialized_vars } => { let shape = state.heap.shape(object_data.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(true); }; let index = index as usize; @@ -1617,7 +1669,7 @@ impl Runtime { }; if dictionary_eligible { let shape = state.heap.shape(state.heap.object(object_id)?.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(true); }; if !shape.entries()[index as usize].flags.configurable { @@ -1633,7 +1685,7 @@ impl Runtime { let (prototype, entries, mut slots, index, configurable) = { let object_data = state.heap.object(object_id)?; let shape = state.heap.shape(object_data.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(true); }; let index = usize::try_from(index) diff --git a/src/engine/object/property_ic.rs b/src/engine/object/property_ic.rs index e4504f9b..c4a18967 100644 --- a/src/engine/object/property_ic.rs +++ b/src/engine/object/property_ic.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use crate::engine::api::{runtime::Runtime, runtime_error::RuntimeError}; -use crate::engine::atom::{Atom, AtomTable}; +use crate::engine::atom::{Atom, AtomIdx, AtomTable}; use crate::engine::code::bytecode::Instruction; use crate::engine::heap::{ContextId, Heap, ObjectId, ObjectKind, PropertySlot, RawValue, ShapeId}; use crate::engine::value::Value; @@ -82,14 +82,14 @@ impl PropertyReadCache { if location.domain != domain || location.realm != realm { return None; } - let object = heap.object(receiver).ok()?; + let object = heap.object_fast(receiver); if !ordinary_receiver(object, location.numeric_key) { return None; } if object.shape != location.shape { return None; } - let shape = heap.shape(object.shape).ok()?; + let shape = heap.shape_fast(object.shape); if shape.layout_revision() != location.revision { return None; } @@ -99,16 +99,11 @@ impl PropertyReadCache { return None; } for _ in 0..location.depth { - let data = heap.object(holder).ok()?; - holder = heap.shape(data.shape).ok()?.prototype()?; + let data = heap.object_fast(holder); + holder = heap.shape_fast(data.shape).prototype()?; } } - match heap - .object(holder) - .ok()? - .slots - .get(location.slot as usize)? - { + match heap.object_fast(holder).slots.get(location.slot as usize)? { PropertySlot::Data(value) => Some(value), // VarRef/AutoInit can share data-shaped storage; never treat them // as immutable data, even if an internal slot writer changed kind. @@ -291,7 +286,7 @@ fn locate( return None; } let shape = heap.shape(data.shape).ok()?; - if let Some(slot) = shape.find(atom) { + if let Some(slot) = shape.find(AtomIdx::from_raw(atom.raw())) { return matches!(data.slots.get(slot as usize), Some(PropertySlot::Data(_))).then_some( Location { domain, @@ -330,7 +325,7 @@ impl PropertyWriteCache { if location.depth != 0 { return None; } - if heap.object(receiver).ok()?.kind == ObjectKind::Array && location.slot == 0 { + if matches!(heap.object(receiver).ok()?.kind, ObjectKind::Array) && location.slot == 0 { return None; } let shape = heap.shape(location.shape).ok()?; diff --git a/src/engine/object/shape.rs b/src/engine/object/shape.rs index 2fff783d..06ae0fce 100644 --- a/src/engine/object/shape.rs +++ b/src/engine/object/shape.rs @@ -17,7 +17,7 @@ //! shape must retain those atoms at the runtime boundary. use super::dictionary_order::DictionaryOrder; -use crate::engine::atom::{Atom, AtomError, AtomTable, PropertyKeyKind}; +use crate::engine::atom::{Atom, AtomError, AtomIdx, AtomTable, PropertyKeyKind}; use crate::engine::heap::ObjectId; use std::collections::HashMap; use std::error::Error; @@ -45,6 +45,10 @@ pub struct PropertyFlags { pub storage: PropertyStorageKind, } +// `PropertyFlags` must stay compact: it shares the 8-byte `ShapeEntry` with +// the `AtomIdx` key. +const _: () = assert!(std::mem::size_of::() == 4); + impl PropertyFlags { /// Construct flags for a data property. #[must_use] @@ -70,12 +74,18 @@ impl PropertyFlags { } /// One entry in a shape's insertion-ordered property metadata. +/// +/// The key is the unbranded [`AtomIdx`]: the shape retains every admitted atom +/// for its own lifetime, so the entry itself needs no brand stamp. Brand +/// checks run at boundaries which receive or expose an entry's key. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ShapeEntry { - pub atom: Atom, + pub atom: AtomIdx, pub flags: PropertyFlags, } +const _: () = assert!(std::mem::size_of::() == 8); + /// Failure to construct a valid immutable shape transition. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ShapeError { @@ -119,7 +129,7 @@ pub struct Shape { layout_revision: u64, prototype: Option, entries: Vec, - lookup: HashMap, + lookup: HashMap, /// Present only for dynamic layouts; shared shapes pay one optional pointer. dictionary_order: Option>, } @@ -161,7 +171,10 @@ impl Shape { } else { lookup.contains_key(&entry.atom) } { - return Err(ShapeError::DuplicateAtom(entry.atom)); + // Entry keys arrive pre-validated at this boundary (callers + // unbrand or convert trusted atoms before constructing), so + // re-stamping the brand is unnecessary for the diagnostic. + return Err(ShapeError::DuplicateAtom(Atom::from_raw(entry.atom.raw()))); } ordered.push(entry); if ordered.len() == 9 { @@ -232,9 +245,11 @@ impl Shape { pub(crate) fn remove_dictionary_property( &mut self, - atom: Atom, + atom: AtomIdx, ) -> Result { - let index = self.find(atom).ok_or(ShapeError::MissingAtom(atom))? as usize; + let index = + self.find(atom) + .ok_or(ShapeError::MissingAtom(Atom::from_raw(atom.raw())))? as usize; self.dictionary_order .as_mut() .expect("dictionary removal requires dictionary metadata") @@ -267,7 +282,7 @@ impl Shape { /// Find a property and return its parallel payload-slot index. #[must_use] - pub fn find(&self, atom: Atom) -> Option { + pub fn find(&self, atom: AtomIdx) -> Option { if self.entries.len() <= 8 { self.entries .iter() @@ -284,12 +299,12 @@ impl Shape { /// non-index string or symbol again places it at the end of its own-key /// category, as required by ECMAScript and QuickJS. #[cfg(test)] - pub fn derive_add(&self, atom: Atom, flags: PropertyFlags) -> Result { + pub fn derive_add(&self, atom: AtomIdx, flags: PropertyFlags) -> Result { if atom.is_null() { return Err(ShapeError::NullAtom); } if self.find(atom).is_some() { - return Err(ShapeError::DuplicateAtom(atom)); + return Err(ShapeError::DuplicateAtom(Atom::from_raw(atom.raw()))); } let index = self.unique_append_index(atom)?; @@ -305,17 +320,22 @@ impl Shape { /// reaching this mutation boundary. Keeping spare `Vec` capacity makes a /// long sequence of unique-object additions amortized linear while shared /// shapes continue to use immutable transitions. - pub(crate) fn unique_append_index(&self, atom: Atom) -> Result { + pub(crate) fn unique_append_index(&self, atom: AtomIdx) -> Result { if atom.is_null() { return Err(ShapeError::NullAtom); } if self.find(atom).is_some() { - return Err(ShapeError::DuplicateAtom(atom)); + return Err(ShapeError::DuplicateAtom(Atom::from_raw(atom.raw()))); } u32::try_from(self.entries.len()).map_err(|_| ShapeError::PropertyIndexOverflow) } - pub(crate) fn append_unique_property(&mut self, atom: Atom, flags: PropertyFlags, index: u32) { + pub(crate) fn append_unique_property( + &mut self, + atom: AtomIdx, + flags: PropertyFlags, + index: u32, + ) { debug_assert_eq!(usize::try_from(index), Ok(self.entries.len())); debug_assert!(!atom.is_null() && self.find(atom).is_none()); self.entries.push(ShapeEntry { atom, flags }); @@ -338,12 +358,15 @@ impl Shape { /// /// Replacement preserves insertion order and payload-slot position. #[cfg(test)] - pub fn derive_replace(&self, atom: Atom, flags: PropertyFlags) -> Result { + pub fn derive_replace(&self, atom: AtomIdx, flags: PropertyFlags) -> Result { if atom.is_null() { return Err(ShapeError::NullAtom); } - let index = usize::try_from(self.find(atom).ok_or(ShapeError::MissingAtom(atom))?) - .map_err(|_| ShapeError::PropertyIndexOverflow)?; + let index = usize::try_from( + self.find(atom) + .ok_or(ShapeError::MissingAtom(Atom::from_raw(atom.raw())))?, + ) + .map_err(|_| ShapeError::PropertyIndexOverflow)?; let mut entries = self.entries.to_vec(); entries[index].flags = flags; Ok(Self { @@ -360,12 +383,15 @@ impl Shape { /// Payload slots after the removed property shift left, so the lookup table /// is rebuilt by the validated constructor. #[cfg(test)] - pub fn derive_delete(&self, atom: Atom) -> Result { + pub fn derive_delete(&self, atom: AtomIdx) -> Result { if atom.is_null() { return Err(ShapeError::NullAtom); } - let index = usize::try_from(self.find(atom).ok_or(ShapeError::MissingAtom(atom))?) - .map_err(|_| ShapeError::PropertyIndexOverflow)?; + let index = usize::try_from( + self.find(atom) + .ok_or(ShapeError::MissingAtom(Atom::from_raw(atom.raw())))?, + ) + .map_err(|_| ShapeError::PropertyIndexOverflow)?; if self.is_dictionary() { let mut result = self.clone(); result.remove_dictionary_property(atom)?; @@ -381,8 +407,9 @@ impl Shape { /// /// Array-index strings come first in ascending numeric order, followed by /// all other strings in insertion order, then symbols in insertion order. - /// Private names are internal and are omitted. Every atom is validated - /// against `atoms`, but this metadata-level operation does not retain it. + /// Private names are internal and are omitted. Every stored key index is + /// fully re-validated (branded) against `atoms` at this public exit, but + /// this metadata-level operation does not retain it. /// /// # Errors /// @@ -395,15 +422,18 @@ impl Shape { for (insertion_index, slot) in self.ordered_indices().enumerate() { let entry = &self.entries[slot]; - match atoms.property_key_kind(entry.atom)? { + // Public exit boundary: the stored unbranded index gets the full + // brand (generation/table-ID) check before leaving the shape. + let atom = atoms.brand(entry.atom)?; + match atoms.property_key_kind(atom)? { PropertyKeyKind::String => { - if let Some(array_index) = atoms.array_index(entry.atom)? { - indices.push((array_index, insertion_index, entry.atom)); + if let Some(array_index) = atoms.array_index(atom)? { + indices.push((array_index, insertion_index, atom)); } else { - strings.push(entry.atom); + strings.push(atom); } } - PropertyKeyKind::Symbol => symbols.push(entry.atom), + PropertyKeyKind::Symbol => symbols.push(atom), PropertyKeyKind::Private => {} } } @@ -429,18 +459,20 @@ mod tests { const DEFAULT_DATA: PropertyFlags = PropertyFlags::data(true, true, true); - fn entry(atom: Atom) -> ShapeEntry { + fn entry(atom: AtomIdx) -> ShapeEntry { ShapeEntry { atom, flags: DEFAULT_DATA, } } + fn immediate(value: u32) -> AtomIdx { + AtomIdx::from_immediate_integer(value).unwrap() + } + #[test] fn small_shape_lookup_allocates_only_above_eight_entries() { - let atoms = (1..=9) - .map(|n| Atom::from_immediate_integer(n).unwrap()) - .collect::>(); + let atoms = (1..=9).map(immediate).collect::>(); let small = Shape::new(None, atoms[..8].iter().copied().map(entry)).unwrap(); assert_eq!(small.lookup.capacity(), 0); for (index, atom) in atoms[..8].iter().enumerate() { @@ -455,22 +487,22 @@ mod tests { #[test] fn constructor_builds_lookup_and_rejects_invalid_entries() { - let first = Atom::from_immediate_integer(1).unwrap(); - let second = Atom::from_immediate_integer(2).unwrap(); + let first = immediate(1); + let second = immediate(2); let shape = Shape::new(None, [entry(first), entry(second)]).unwrap(); assert_eq!(shape.entries(), &[entry(first), entry(second)]); assert_eq!(shape.find(first), Some(0)); assert_eq!(shape.find(second), Some(1)); - assert_eq!(shape.find(Atom::from_immediate_integer(3).unwrap()), None); + assert_eq!(shape.find(immediate(3)), None); assert!(shape.prototype().is_none()); assert!(matches!( Shape::new(None, [entry(first), entry(first)]), - Err(ShapeError::DuplicateAtom(atom)) if atom == first + Err(ShapeError::DuplicateAtom(atom)) if atom.raw() == first.raw() )); assert!(matches!( - Shape::new(None, [entry(Atom::NULL)]), + Shape::new(None, [entry(AtomIdx::NULL)]), Err(ShapeError::NullAtom) )); } @@ -478,8 +510,8 @@ mod tests { #[test] fn transitions_preserve_or_update_insertion_positions() { let mut atoms = AtomTable::new(); - let alpha = atoms.intern("alpha").unwrap(); - let beta = atoms.intern("beta").unwrap(); + let alpha = AtomIdx::from_raw(atoms.intern("alpha").unwrap().raw()); + let beta = AtomIdx::from_raw(atoms.intern("beta").unwrap().raw()); let shape = Shape::new(None, [entry(alpha), entry(beta)]).unwrap(); let accessor = PropertyFlags::accessor(false, true); @@ -496,31 +528,34 @@ mod tests { assert!(matches!( readded.derive_add(alpha, DEFAULT_DATA), - Err(ShapeError::DuplicateAtom(atom)) if atom == alpha + Err(ShapeError::DuplicateAtom(atom)) if atom.raw() == alpha.raw() )); - let missing = atoms.intern("missing").unwrap(); + let missing = AtomIdx::from_raw(atoms.intern("missing").unwrap().raw()); assert!(matches!( readded.derive_replace(missing, DEFAULT_DATA), - Err(ShapeError::MissingAtom(atom)) if atom == missing + Err(ShapeError::MissingAtom(atom)) if atom.raw() == missing.raw() )); assert!(matches!( readded.derive_delete(missing), - Err(ShapeError::MissingAtom(atom)) if atom == missing + Err(ShapeError::MissingAtom(atom)) if atom.raw() == missing.raw() )); } #[test] fn own_keys_follow_array_string_symbol_order_and_hide_private_names() { let mut atoms = AtomTable::new(); - let beta = atoms.intern("beta").unwrap(); - let index_10 = atoms.intern("10").unwrap(); - let symbol_a = atoms.new_symbol(Some("a")).unwrap(); - let index_2 = atoms.intern("2").unwrap(); - let noncanonical_index = atoms.intern("01").unwrap(); - let private = atoms.new_private_symbol(Some("hidden")).unwrap(); - let global_symbol = atoms.intern_global_symbol("shared").unwrap(); - let largest_index = atoms.intern("4294967294").unwrap(); - let excluded_index = atoms.intern("4294967295").unwrap(); + let key = |atoms: &mut AtomTable, text: &str| { + AtomIdx::from_raw(atoms.intern(text).unwrap().raw()) + }; + let beta = key(&mut atoms, "beta"); + let index_10 = key(&mut atoms, "10"); + let symbol_a = AtomIdx::from_raw(atoms.new_symbol(Some("a")).unwrap().raw()); + let index_2 = key(&mut atoms, "2"); + let noncanonical_index = key(&mut atoms, "01"); + let private = AtomIdx::from_raw(atoms.new_private_symbol(Some("hidden")).unwrap().raw()); + let global_symbol = AtomIdx::from_raw(atoms.intern_global_symbol("shared").unwrap().raw()); + let largest_index = key(&mut atoms, "4294967294"); + let excluded_index = key(&mut atoms, "4294967295"); let shape = Shape::new( None, @@ -539,17 +574,18 @@ mod tests { ) .unwrap(); + let branded = |index: AtomIdx| atoms.brand(index).unwrap(); assert_eq!( shape.ordered_own_keys(&atoms).unwrap(), vec![ - index_2, - index_10, - largest_index, - beta, - noncanonical_index, - excluded_index, - symbol_a, - global_symbol, + branded(index_2), + branded(index_10), + branded(largest_index), + branded(beta), + branded(noncanonical_index), + branded(excluded_index), + branded(symbol_a), + branded(global_symbol), ] ); } @@ -557,8 +593,8 @@ mod tests { #[test] fn delete_then_readd_moves_non_index_key_to_category_end() { let mut atoms = AtomTable::new(); - let first = atoms.intern("first").unwrap(); - let second = atoms.intern("second").unwrap(); + let first = AtomIdx::from_raw(atoms.intern("first").unwrap().raw()); + let second = AtomIdx::from_raw(atoms.intern("second").unwrap().raw()); let shape = Shape::new(None, [entry(first), entry(second)]).unwrap(); let changed = shape .derive_delete(first) @@ -566,20 +602,27 @@ mod tests { .derive_add(first, DEFAULT_DATA) .unwrap(); - assert_eq!(changed.ordered_own_keys(&atoms).unwrap(), [second, first]); - assert_eq!(shape.ordered_own_keys(&atoms).unwrap(), [first, second]); + let branded = |index: AtomIdx| atoms.brand(index).unwrap(); + assert_eq!( + changed.ordered_own_keys(&atoms).unwrap(), + [branded(second), branded(first)] + ); + assert_eq!( + shape.ordered_own_keys(&atoms).unwrap(), + [branded(first), branded(second)] + ); } #[test] fn own_key_snapshot_validates_the_runtime_local_atom_table() { let mut owner = AtomTable::new(); let foreign = AtomTable::new(); - let key = owner.intern("owner-only").unwrap(); + let key = AtomIdx::from_raw(owner.intern("owner-only").unwrap().raw()); let shape = Shape::new(None, [entry(key)]).unwrap(); assert!(matches!( shape.ordered_own_keys(&foreign), - Err(AtomError::UnknownAtom(atom)) if atom == key + Err(AtomError::UnknownAtom(atom)) if atom.raw() == key.raw() )); } } diff --git a/src/engine/object/storage.rs b/src/engine/object/storage.rs index 355fa668..6d1871ba 100644 --- a/src/engine/object/storage.rs +++ b/src/engine/object/storage.rs @@ -1,6 +1,6 @@ use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::code::function::metadata::ClosureVariableKind; use crate::engine::heap::roots::VarRefRoot; use crate::engine::heap::runtime::RuntimeState; @@ -11,7 +11,7 @@ use crate::engine::object::{ AccessorValue, CompleteOrdinaryPropertyDescriptor, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, properties, }; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; /// One-use missing selection minted only by the storage transaction below. /// It never leaves that exclusive RuntimeState borrow or enters a VM/JS state. @@ -142,6 +142,52 @@ impl Runtime { } } + /// Internal-value form of [`Runtime::value_to_boolean`]. + pub(crate) fn value_to_boolean_jsvalue(&self, value: &JsValue) -> Result { + match value { + JsValue::Object(id) => Ok(!self.0.state.borrow().heap.object(*id)?.is_html_dda), + JsValue::String(id) => Ok(!self.0.state.borrow().heap.string(*id)?.is_empty()), + JsValue::BigInt(id) => Ok(!self.0.state.borrow().heap.bigint(*id)?.is_zero()), + _ => Ok(value.to_boolean_primitive()), + } + } + + /// Strict equality over internal values: handle identity is the fast path; + /// string and BigInt handles fall back to arena content comparison. + pub(crate) fn strict_equal_jsvalue( + &self, + left: &JsValue, + right: &JsValue, + ) -> Result { + Ok(match (left, right) { + (JsValue::Undefined, JsValue::Undefined) | (JsValue::Null, JsValue::Null) => true, + (JsValue::Bool(left), JsValue::Bool(right)) => left == right, + (JsValue::Int(left), JsValue::Int(right)) => left == right, + (JsValue::Symbol(left), JsValue::Symbol(right)) => left == right, + (JsValue::Object(left), JsValue::Object(right)) => left == right, + (JsValue::String(left), JsValue::String(right)) => { + if left == right { + true + } else { + let state = self.0.state.borrow(); + state.heap.string(*left)? == state.heap.string(*right)? + } + } + (JsValue::BigInt(left), JsValue::BigInt(right)) => { + if left == right { + true + } else { + let state = self.0.state.borrow(); + state.heap.bigint(*left)? == state.heap.bigint(*right)? + } + } + (left, right) => match (left.as_number(), right.as_number()) { + (Some(left), Some(right)) => left == right, + _ => false, + }, + }) + } + /// Mirror `JS_SetIsHTMLDDA` for a runtime-owned object. #[cfg(feature = "test262-host")] pub(crate) fn set_object_is_html_dda(&self, object: &ObjectRef) -> Result<(), RuntimeError> { @@ -156,6 +202,19 @@ impl Runtime { Ok(()) } + /// Convert a public value into its heap-stored form at a boundary. + /// + /// String and BigInt payloads allocate one arena node each (API input + /// conversion is a genuine creation point); the returned value carries + /// that one producer-owned node edge, which the caller releases with + /// [`Runtime::release_converted_value_edge`] once a transactional store + /// has retained its own copy edge (or immediately when nothing stores + /// the value). Object edges and Symbol atoms are *not* retained here — + /// the historical contract stands: transactional stores retain object and + /// string/BigInt edges, and Symbol/Private atoms are retained by the + /// store's atom accounting or explicitly by transfer points. + /// + /// This conversion takes its own state borrow; callers must not hold one. pub(crate) fn raw_property_value(&self, value: &Value) -> Result { Ok(match value { Value::Undefined => RawValue::Undefined, @@ -163,13 +222,32 @@ impl Runtime { Value::Bool(value) => RawValue::Bool(*value), Value::Int(value) => RawValue::Int(*value), Value::Float(value) => RawValue::Float(*value), - Value::BigInt(value) => RawValue::BigInt(value.clone()), - Value::String(value) => RawValue::String(value.clone()), + Value::BigInt(value) => { + let mut state = self.0.state.borrow_mut(); + let id = state.heap.allocate_bigint(value.clone())?; + #[cfg(debug_assertions)] + if std::env::var("QJS_TRACE_BIGINT_ID") + .is_ok_and(|value| format!("{id:?}").contains(&format!("index: {value},"))) + { + eprintln!( + "[raw-b] {id:?}\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + RawValue::BigInt(id) + } + Value::String(value) => { + let mut state = self.0.state.borrow_mut(); + let id = state.heap.allocate_string(value.clone())?; + RawValue::String(id) + } Value::Symbol(symbol) => { if !symbol.belongs_to(self) { return Err(RuntimeError::WrongRuntime("property value")); } - RawValue::Symbol(symbol.atom()) + let atom = symbol.atom(); + let index = self.0.state.borrow().atoms.unbrand(atom)?; + RawValue::Symbol(index) } Value::Object(object) => { if !object.belongs_to(self) { @@ -232,16 +310,22 @@ impl Runtime { return self.store_complete_global_property(object, hidden, key, complete); } - let (flags, replacement) = match complete { + // Clone duplicates only the handle; the probe keeps the boundary + // conversion's producer edge accountable through the store below. + let (flags, replacement, value_probe) = match complete { CompleteOrdinaryPropertyDescriptor::Data { value, writable, enumerable, configurable, - } => ( - PropertyFlags::data(writable, enumerable, configurable), - PropertySlot::Data(self.raw_property_value(&value)?), - ), + } => { + let raw = self.raw_property_value(&value)?; + ( + PropertyFlags::data(writable, enumerable, configurable), + PropertySlot::Data(raw.clone()), + Some(raw), + ) + } CompleteOrdinaryPropertyDescriptor::Accessor { get, set, @@ -253,9 +337,16 @@ impl Runtime { get: get.as_ref().map(|value| value.as_object().object_id()), set: set.as_ref().map(|value| value.as_object().object_id()), }, + None, ), }; - self.store_property_slot(object, key, flags, replacement) + let stored = self.store_property_slot(object, key, flags, replacement); + // The store retained its own copy edge on success; a rejected store + // never kept the value. Balance the producer edge either way. + if let Some(raw) = &value_probe { + self.release_converted_value_edge(raw); + } + stored } pub(crate) fn store_complete_global_property( @@ -280,7 +371,7 @@ impl Runtime { None }; let root = if let Some(root) = global_root { - self.write_var_ref(&root, value)?; + self.write_var_ref(&root, self.unroot_value(&value)?)?; root } else if let Some(root) = hidden_root { if !self.delete_property(&hidden, key)? { @@ -288,10 +379,10 @@ impl Runtime { "hidden global VarRef property was not configurable", )); } - self.write_var_ref(&root, value)?; + self.write_var_ref(&root, self.unroot_value(&value)?)?; root } else { - self.new_var_ref(value, false, !writable, ClosureVariableKind::Normal)? + self.new_var_ref_rooted(value, false, !writable, ClosureVariableKind::Normal)? }; self.set_var_ref_metadata(&root, false, !writable, ClosureVariableKind::Normal)?; self.store_property_slot( @@ -353,7 +444,7 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(object.object_id())?; let shape = state.heap.shape(object.shape)?; - let Some(index) = shape.find(key.atom()) else { + let Some(index) = shape.find(AtomIdx::from_raw(key.atom().raw())) else { return Ok(None); }; match object.slots.get(index as usize) { @@ -387,7 +478,7 @@ impl Runtime { let object_data = state.heap.object(object_id)?; let shape = state.heap.shape(object_data.shape)?; shape - .find(key.atom()) + .find(AtomIdx::from_raw(key.atom().raw())) .map(|index| { let index = index as usize; let entry = shape.entries().get(index).ok_or(RuntimeError::Invariant( @@ -464,7 +555,13 @@ impl RuntimeState { ); } if existing.is_none() && !dictionary { - let target = state.append_transition(shape_id, ShapeEntry { atom, flags })?; + let target = state.append_transition( + shape_id, + ShapeEntry { + atom: AtomIdx::from_raw(atom.raw()), + flags, + }, + )?; let mut slots = state.heap.object(object_id)?.slots.clone(); slots.push(replacement); return state.replace_layout_with_owned_shape(object_id, target, slots); @@ -485,7 +582,10 @@ impl RuntimeState { entries[index].flags = flags; slots[index] = replacement; } else { - entries.push(ShapeEntry { atom, flags }); + entries.push(ShapeEntry { + atom: AtomIdx::from_raw(atom.raw()), + flags, + }); slots.push(replacement); } state.replace_layout(object_id, prototype, &entries, slots) @@ -510,7 +610,14 @@ mod selected_append_tests { let mut state = runtime.0.state.borrow_mut(); let shape = state.heap.object(owner.object_id()).unwrap().shape; assert_eq!(state.heap.shape_strong_count(shape), Ok(1)); - assert!(state.heap.shape(shape).unwrap().find(key.atom()).is_none()); + assert!( + state + .heap + .shape(shape) + .unwrap() + .find(AtomIdx::from_raw(key.atom().raw())) + .is_none() + ); let before_atoms = state.atoms.resolve(key.atom()).unwrap().ref_count; let fingerprint = state.shape_fingerprints.get(&shape).unwrap().clone(); let selected = SelectedMissingAppend { diff --git a/src/engine/realm/bindings.rs b/src/engine/realm/bindings.rs index b4852b56..7790313c 100644 --- a/src/engine/realm/bindings.rs +++ b/src/engine/realm/bindings.rs @@ -1,7 +1,7 @@ use crate::engine::api::error::ErrorKind; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; +use crate::engine::atom::{Atom, AtomIdx}; use crate::engine::code::function::metadata::ClosureVariableKind; use crate::engine::heap::roots::VarRefRoot; @@ -10,7 +10,7 @@ use crate::engine::object::shape::PropertyFlags; use crate::engine::object::{ObjectRef, PropertyKey}; #[cfg(test)] use crate::engine::value::JsString; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; impl Runtime { pub(crate) fn check_global_lexical_declaration( @@ -25,9 +25,11 @@ impl Runtime { let lexical_shape = state.heap.shape(lexical.shape)?; let global = state.heap.object(context.global_object)?; let global_shape = state.heap.shape(global.shape)?; - let lexical_exists = lexical_shape.find(key.atom()).is_some(); + let lexical_exists = lexical_shape + .find(AtomIdx::from_raw(key.atom().raw())) + .is_some(); let fixed_global_exists = global_shape - .find(key.atom()) + .find(AtomIdx::from_raw(key.atom().raw())) .and_then(|index| global_shape.entries().get(index as usize)) .is_some_and(|entry| !entry.flags.configurable); lexical_exists || fixed_global_exists @@ -52,9 +54,16 @@ impl Runtime { let lexical_shape = state.heap.shape(lexical.shape)?; let global = state.heap.object(context.global_object)?; let global_shape = state.heap.shape(global.shape)?; - if global_shape.find(key.atom()).is_none() && !global.extensible { + if global_shape + .find(AtomIdx::from_raw(key.atom().raw())) + .is_none() + && !global.extensible + { Some(ErrorKind::Type) - } else if lexical_shape.find(key.atom()).is_some() { + } else if lexical_shape + .find(AtomIdx::from_raw(key.atom().raw())) + .is_some() + { Some(ErrorKind::Syntax) } else { None @@ -92,7 +101,7 @@ impl Runtime { let global = state.heap.object(context.global_object)?; let global_shape = state.heap.shape(global.shape)?; let cannot_define = - match global_shape.find(key.atom()) { + match global_shape.find(AtomIdx::from_raw(key.atom().raw())) { None => !global.extensible, Some(index) => { let index = usize::try_from(index).map_err(|_| { @@ -112,7 +121,10 @@ impl Runtime { }; if cannot_define { Some(ErrorKind::Type) - } else if lexical_shape.find(key.atom()).is_some() { + } else if lexical_shape + .find(AtomIdx::from_raw(key.atom().raw())) + .is_some() + { Some(ErrorKind::Syntax) } else { None @@ -259,16 +271,20 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(global_object.object_id())?; let shape = state.heap.shape(object.shape)?; - let index = shape.find(key.atom()).ok_or(RuntimeError::Invariant( - "global VarRef disappeared during lexical creation", - ))? as usize; + let index = shape.find(AtomIdx::from_raw(key.atom().raw())).ok_or( + RuntimeError::Invariant("global VarRef disappeared during lexical creation"), + )? as usize; let flags = shape.entries()[index].flags; let value = state.heap.var_ref(root.id())?.value.clone(); (flags, value) }; let value = self.root_raw_value(&value)?; - let replacement = - self.new_var_ref(value, false, !flags.writable, ClosureVariableKind::Normal)?; + let replacement = self.new_var_ref_rooted( + value, + false, + !flags.writable, + ClosureVariableKind::Normal, + )?; self.store_property_slot( &global_object, key, @@ -289,7 +305,7 @@ impl Runtime { }; self.set_var_ref_metadata(&root, true, is_const, ClosureVariableKind::Normal)?; if let Some(value) = initial_value { - self.write_var_ref(&root, value)?; + self.write_var_ref(&root, self.into_jsvalue(value)?)?; } self.store_property_slot( &global_var_object, @@ -344,7 +360,7 @@ impl Runtime { "hidden global VarRef property was not configurable", )); } - self.write_var_ref(&root, Value::Undefined)?; + self.write_var_ref(&root, JsValue::Undefined)?; self.set_var_ref_metadata(&root, false, false, ClosureVariableKind::Normal)?; self.store_property_slot( &global_object, @@ -400,7 +416,7 @@ impl Runtime { "hidden global VarRef property was not configurable", )); } - self.write_var_ref(&root, Value::Undefined)?; + self.write_var_ref(&root, JsValue::Undefined)?; self.set_var_ref_metadata(&root, false, false, ClosureVariableKind::Normal)?; self.store_property_slot( &global_object, @@ -420,9 +436,11 @@ impl Runtime { let state = self.0.state.borrow(); let object = state.heap.object(global_object.object_id())?; let shape = state.heap.shape(object.shape)?; - let index = usize::try_from(shape.find(key.atom()).ok_or(RuntimeError::Invariant( - "global function property disappeared after declaration creation", - ))?) + let index = usize::try_from(shape.find(AtomIdx::from_raw(key.atom().raw())).ok_or( + RuntimeError::Invariant( + "global function property disappeared after declaration creation", + ), + )?) .map_err(|_| RuntimeError::Invariant("shape index does not fit usize"))?; let flags = shape .entries() @@ -456,7 +474,7 @@ impl Runtime { } } PropertySlot::Data(value) => { - let value = self.root_raw_value(value)?; + let value = self.into_jsvalue(self.root_raw_value(value)?)?; self.write_var_ref(&root, value)?; if hidden_root .as_ref() @@ -542,7 +560,7 @@ impl Runtime { .ok_or(RuntimeError::Invariant( "test initialized a missing global lexical binding", ))?; - self.write_var_ref(&root, value) + self.write_var_ref(&root, self.into_jsvalue(value)?) } } diff --git a/src/engine/realm/mod.rs b/src/engine/realm/mod.rs index 19198042..83449dcf 100644 --- a/src/engine/realm/mod.rs +++ b/src/engine/realm/mod.rs @@ -118,12 +118,14 @@ impl Runtime { for name in ["parseInt", "parseFloat"] { let key = self.intern_property_key(name)?; let value = match self.get_property_in_realm(realm, global_object, &key)? { - Completion::Return(value @ Value::Object(_)) => value, - Completion::Return(_) => { - return Err(RuntimeError::Invariant( - "global numeric parser was not an object during Number bootstrap", - )); - } + Completion::Return(value) => match self.root_and_release_jsvalue(value)? { + value @ Value::Object(_) => value, + _ => { + return Err(RuntimeError::Invariant( + "global numeric parser was not an object during Number bootstrap", + )); + } + }, Completion::Throw(_) => { return Err(RuntimeError::Invariant( "global numeric parser lookup threw during Number bootstrap", diff --git a/src/engine/value/bigint.rs b/src/engine/value/bigint.rs index 74c9cba5..e709665c 100644 --- a/src/engine/value/bigint.rs +++ b/src/engine/value/bigint.rs @@ -116,6 +116,7 @@ pub struct JsBigInt(BigIntRepr); impl JsBigInt { /// Immediate limbs have no allocation; shared heap limbs survive one drop. + #[cfg_attr(not(test), allow(dead_code))] pub(crate) fn release_keeps_storage_alive(&self) -> bool { match &self.0 { BigIntRepr::Short(_) => true, diff --git a/src/engine/value/collection_key.rs b/src/engine/value/collection_key.rs index 4a1483ab..74d5ac66 100644 --- a/src/engine/value/collection_key.rs +++ b/src/engine/value/collection_key.rs @@ -1,9 +1,12 @@ -//! Pure SameValueZero key operations on validated, runtime-local raw values. +//! SameValueZero key operations on validated, runtime-local raw values. //! -//! No rooting, heap access, coercion or user callbacks occur here. Callers must -//! reject internal sentinels and foreign identities at their storage boundary. +//! String and BigInt keys are generational node handles, so equality and +//! hashing take the heap: same-kind handles compare by identity first and fall +//! back to node content, exactly the semantics the storage layer guarantees. +//! Callers must reject internal sentinels and foreign identities at their +//! storage boundary; keys owned by live collection records always resolve. -use crate::engine::heap::RawValue; +use crate::engine::heap::{BigIntId, Heap, RawValue, StringId}; use std::hash::{Hash, Hasher}; fn number(value: &RawValue) -> Option { @@ -14,15 +17,31 @@ fn number(value: &RawValue) -> Option { } } -pub(crate) fn same_value_zero(left: &RawValue, right: &RawValue) -> bool { +/// Live node payloads compare by content; stale handles are an invariant +/// violation at every validated-key call site. +fn string_content(heap: &Heap, id: StringId) -> &crate::engine::value::JsString { + heap.string(id) + .expect("a live collection key resolves its string node") +} + +fn bigint_content(heap: &Heap, id: BigIntId) -> &crate::engine::value::bigint::JsBigInt { + heap.bigint(id) + .expect("a live collection key resolves its bigint node") +} + +pub(crate) fn same_value_zero(heap: &Heap, left: &RawValue, right: &RawValue) -> bool { if let (Some(left), Some(right)) = (number(left), number(right)) { return left == right || (left.is_nan() && right.is_nan()); } match (left, right) { (RawValue::Undefined, RawValue::Undefined) | (RawValue::Null, RawValue::Null) => true, (RawValue::Bool(left), RawValue::Bool(right)) => left == right, - (RawValue::String(left), RawValue::String(right)) => left == right, - (RawValue::BigInt(left), RawValue::BigInt(right)) => left == right, + (RawValue::String(left), RawValue::String(right)) => { + left == right || string_content(heap, *left) == string_content(heap, *right) + } + (RawValue::BigInt(left), RawValue::BigInt(right)) => { + left == right || bigint_content(heap, *left) == bigint_content(heap, *right) + } (RawValue::Symbol(left), RawValue::Symbol(right)) => left == right, (RawValue::Object(left), RawValue::Object(right)) => left == right, _ => false, @@ -31,16 +50,16 @@ 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 { +pub(crate) fn same_value(heap: &Heap, 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) + same_value_zero(heap, left, right) } -pub(crate) fn hash(key: &RawValue, state: &mut H) { +pub(crate) fn hash(heap: &Heap, key: &RawValue, state: &mut H) { match key { RawValue::Undefined => state.write_u8(0), RawValue::Null => state.write_u8(1), @@ -62,16 +81,17 @@ pub(crate) fn hash(key: &RawValue, state: &mut H) { }; state.write_u64(bits); } - RawValue::String(value) => { + RawValue::String(id) => { state.write_u8(4); + let value = string_content(heap, *id); value.len().hash(state); // Hash actual content with the index's randomized hasher, not the // existing unseeded 32-bit content fingerprint. value.hash_code_units(state); } - RawValue::BigInt(value) => { + RawValue::BigInt(id) => { state.write_u8(5); - value.hash(state); + bigint_content(heap, *id).hash(state); } RawValue::Symbol(value) => { state.write_u8(6); diff --git a/src/engine/value/conversion.rs b/src/engine/value/conversion.rs index 469c534e..ce0698f7 100644 --- a/src/engine/value/conversion.rs +++ b/src/engine/value/conversion.rs @@ -23,8 +23,12 @@ impl Runtime { ) -> Result, RuntimeError> { let value = if matches!(value, Value::Object(_)) { match self.to_primitive(realm, value, ToPrimitiveHint::String)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } } } else { value @@ -32,6 +36,76 @@ impl Runtime { self.property_key_from_primitive(realm, value) } + /// Internal-value form of [`Runtime::native_to_property_key`]. + pub(crate) fn native_to_property_key_jsvalue( + &self, + realm: ContextId, + value: crate::engine::value::JsValue, + ) -> Result, RuntimeError> { + let value = if matches!(value, crate::engine::value::JsValue::Object(_)) { + match self.to_primitive_jsvalue(realm, value, ToPrimitiveHint::String)? { + Completion::Return(value) => value, + Completion::Throw(value) => { + // The callback boundary throws public roots; hand the host + // adapter its owned root back and release the internal edge. + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } + } + } else { + value + }; + self.property_key_from_primitive_jsvalue(realm, value) + } + + /// Internal-value form of [`Runtime::property_key_from_primitive`]. + /// + /// Consumes one owned internal value edge and releases it on every path. + pub(crate) fn property_key_from_primitive_jsvalue( + &self, + realm: ContextId, + value: crate::engine::value::JsValue, + ) -> Result, RuntimeError> { + let result = (|| { + use crate::engine::value::JsValue; + if matches!(value, JsValue::Object(_)) { + return Err(RuntimeError::Invariant( + "property key conversion received an object", + )); + } + if let Some(key) = self.immediate_numeric_property_key_jsvalue(&value) { + return Ok(NativeConversion::Value(key)); + } + if let JsValue::Symbol(index) = &value { + let atom = self.0.state.borrow().atoms.brand(*index)?; + return Ok(NativeConversion::Value(PropertyKey::from_borrowed_atom( + self.clone(), + atom, + )?)); + } + let string = match crate::engine::vm::to_js_string_jsvalue(self, &value) { + Ok(string) => string, + Err(error) => { + let Some(kind) = NativeErrorKind::from_javascript_error(error.kind()) else { + return Err(RuntimeError::Engine(error)); + }; + return Ok(NativeConversion::Throw( + self.new_native_error_from_error(realm, kind, &error)?, + )); + } + }; + Ok(NativeConversion::Value( + self.intern_property_key_js_string(&string)?, + )) + })(); + match (result, self.release_jsvalue(value)) { + (Ok(conversion), Ok(())) => Ok(conversion), + (Err(error), _) => Err(error), + (Ok(_), Err(error)) => Err(error), + } + } + /// Finish ToPropertyKey after the domain continuation has obtained a primitive. pub(crate) fn property_key_from_primitive( &self, @@ -96,7 +170,7 @@ impl Runtime { DescriptorStep::Read { mut resume } => { let object = resume.take_read_object(); let key = resume.take_read_key(); - let receiver = resume.take_read_receiver(); + let receiver = self.root_and_release_jsvalue(resume.take_read_receiver())?; resume.read(self, self.internal_get(realm, &object, &key, receiver)?)? } }; @@ -110,8 +184,12 @@ impl Runtime { ) -> Result, RuntimeError> { let value = if matches!(value, Value::Object(_)) { match self.to_primitive(realm, value.clone(), ToPrimitiveHint::String)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } } } else { value.clone() @@ -166,8 +244,12 @@ impl Runtime { } number::NumberStep::Call { mut resume } => { let callable = resume.take_call_callable(); - let receiver = resume.take_call_receiver(); - let arguments = resume.take_call_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_call_receiver())?; + let arguments = resume + .take_call_arguments() + .into_iter() + .map(|argument| self.root_and_release_jsvalue(argument)) + .collect::, _>>()?; resume.resume( self, self.call_internal(realm, &callable, receiver, &arguments)?, @@ -274,8 +356,12 @@ impl Runtime { ) -> Result, RuntimeError> { let value = if matches!(value, Value::Object(_)) { match self.to_primitive(realm, value.clone(), ToPrimitiveHint::Number)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } } } else { value.clone() @@ -471,6 +557,17 @@ impl Runtime { realm: ContextId, value: Value, hint: ToPrimitiveHint, + ) -> Result { + let step = primitive::PrimitiveResume::start(self, realm, self.unroot_value(&value)?, hint); + self.finish_primitive_steps(realm, step) + } + + /// Internal-value form of [`Runtime::to_primitive`]: consumes the value. + pub(crate) fn to_primitive_jsvalue( + &self, + realm: ContextId, + value: crate::engine::value::JsValue, + hint: ToPrimitiveHint, ) -> Result { let step = primitive::PrimitiveResume::start(self, realm, value, hint); self.finish_primitive_steps(realm, step) @@ -501,6 +598,39 @@ impl Runtime { self.new_primitive_object(&prototype, kind, value)?, )) } + + /// Internal-value form of [`Runtime::native_to_object`]: consumes the value. + pub(crate) fn native_to_object_jsvalue( + &self, + realm: ContextId, + value: crate::engine::value::JsValue, + ) -> Result, RuntimeError> { + use crate::engine::value::JsValue; + let (kind, value) = match value { + JsValue::Object(object) => { + return Ok(NativeConversion::Value(ObjectRef::from_owned_handle( + self.clone(), + object, + ))); + } + JsValue::Undefined | JsValue::Null => { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Type, + "cannot convert to object", + )?)); + } + value @ JsValue::Bool(_) => (PrimitiveKind::Boolean, value), + value @ (JsValue::Int(_) | JsValue::Float(_)) => (PrimitiveKind::Number, value), + value @ JsValue::String(_) => (PrimitiveKind::String, value), + value @ JsValue::BigInt(_) => (PrimitiveKind::BigInt, value), + value @ JsValue::Symbol(_) => (PrimitiveKind::Symbol, value), + }; + let prototype = self.primitive_prototype_for_realm(realm, kind)?; + Ok(NativeConversion::Value( + self.new_primitive_object_jsvalue(&prototype, kind, value)?, + )) + } } pub(crate) enum NativeConversion { diff --git a/src/engine/value/conversion/descriptor.rs b/src/engine/value/conversion/descriptor.rs index 5efcc72d..57765c02 100644 --- a/src/engine/value/conversion/descriptor.rs +++ b/src/engine/value/conversion/descriptor.rs @@ -4,7 +4,7 @@ use crate::engine::heap::ContextId; use crate::engine::object::{ AccessorValue, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, }; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::Completion; pub(crate) enum DescriptorStep { @@ -61,7 +61,7 @@ impl DescriptorStep { return invalid(runtime, realm, "not an object"); }; DescriptorResume(Box::new(DescriptorResumeState { - pending_effect: DescriptorStepPending::default(), + pending_effect: DescriptorStepPending::new(runtime), state: State { realm, object, @@ -127,7 +127,7 @@ impl DescriptorResume { return self.next(runtime); } let object = state.object.clone(); - let receiver = Value::Object(state.object.clone()); + let receiver = JsValue::Object(state.object.clone().into_handle()); Ok(DescriptorStep::request_read(object, key, receiver, self)) } @@ -143,7 +143,7 @@ impl DescriptorResume { } let state = &mut self.0.state; let value = match completion { - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, Completion::Throw(value) => { if state.field >= 4 { return invalid( @@ -156,7 +156,9 @@ impl DescriptorResume { }, ); } - return Ok(DescriptorStep::Throw(value)); + return Ok(DescriptorStep::Throw( + runtime.root_and_release_jsvalue(value)?, + )); } }; match state.field { @@ -219,13 +221,15 @@ mod tests { resume } - fn take_read(step: DescriptorStep) -> DescriptorResume { + fn take_read(runtime: &Runtime, step: DescriptorStep) -> DescriptorResume { let DescriptorStep::Read { mut resume } = step else { panic!("expected value read"); }; drop(resume.take_read_object()); drop(resume.take_read_key()); - drop(resume.take_read_receiver()); + runtime + .release_jsvalue(resume.take_read_receiver()) + .unwrap(); resume } @@ -275,12 +279,16 @@ mod tests { .unwrap(); } let resume = take_read( + &runtime, take_has(step) .has(&runtime, NativeConversion::Value(true)) .unwrap(), ); let step = resume - .read(&runtime, Completion::Return(Value::Object(value))) + .read( + &runtime, + Completion::Return(JsValue::Object(value.into_handle())), + ) .unwrap(); runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(object_id).is_ok()); @@ -309,7 +317,7 @@ mod tests { drop(resume.take_has_key()); assert!( resume - .read(&runtime, Completion::Return(Value::Int(1))) + .read(&runtime, Completion::Return(JsValue::Int(1))) .is_err() ); runtime.run_gc().unwrap(); @@ -317,13 +325,35 @@ mod tests { } } -#[derive(Default)] struct DescriptorStepPending { + runtime: Runtime, has_object: Option, has_key: Option, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, +} +impl DescriptorStepPending { + fn new(runtime: &Runtime) -> Self { + Self { + runtime: runtime.clone(), + has_object: None, + has_key: None, + read_object: None, + read_key: None, + read_receiver: None, + } + } +} +impl Drop for DescriptorStepPending { + /// Release the internal edges still held when the descriptor request is + /// abandoned before conversion. Consumption goes through `Option::take`; + /// releases are defer-safe and nothrow, and never run JavaScript. + fn drop(&mut self) { + if let Some(value) = self.read_receiver.take() { + let _ = self.runtime.release_jsvalue(value); + } + } } impl DescriptorStep { pub(crate) fn request_has( @@ -338,7 +368,7 @@ impl DescriptorStep { pub(crate) fn request_read( object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, mut resume: DescriptorResume, ) -> Self { resume.0.pending_effect.read_object = Some(object); @@ -376,7 +406,7 @@ impl DescriptorResume { .take() .expect("DescriptorStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver diff --git a/src/engine/value/conversion/number.rs b/src/engine/value/conversion/number.rs index 104801e1..56e42558 100644 --- a/src/engine/value/conversion/number.rs +++ b/src/engine/value/conversion/number.rs @@ -2,6 +2,7 @@ use super::primitive::{PrimitiveResume, PrimitiveStep}; use super::*; use crate::engine::object::CallableRef; +use crate::engine::value::JsValue; pub(crate) enum NumberStep { Complete(NativeConversion), @@ -35,7 +36,12 @@ impl NumberStep { from_primitive( runtime, realm, - PrimitiveResume::start(runtime, realm, value, ToPrimitiveHint::Number), + PrimitiveResume::start( + runtime, + realm, + runtime.unroot_value(&value)?, + ToPrimitiveHint::Number, + ), ) } } @@ -45,10 +51,11 @@ fn from_primitive( step: PrimitiveStep, ) -> Result { Ok(match step { - PrimitiveStep::Complete(Completion::Throw(value)) => { - NumberStep::Complete(NativeConversion::Throw(value)) - } + PrimitiveStep::Complete(Completion::Throw(value)) => NumberStep::Complete( + NativeConversion::Throw(runtime.root_and_release_jsvalue(value)?), + ), PrimitiveStep::Complete(Completion::Return(value)) => { + let value = runtime.root_and_release_jsvalue(value)?; NumberStep::Complete(runtime.number_from_primitive(realm, &value)?) } PrimitiveStep::Get { mut resume } => { @@ -99,8 +106,8 @@ struct NumberStepPending { read_object: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, + call_receiver: Option, + call_arguments: Option>, } impl NumberStep { pub(crate) fn request_read( @@ -114,8 +121,8 @@ impl NumberStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: NumberResume, ) -> Self { resume.0.pending_effect.call_callable = Some(callable); @@ -146,14 +153,14 @@ impl NumberResume { .take() .expect("NumberStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.0 .pending_effect .call_receiver .take() .expect("NumberStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.0 .pending_effect .call_arguments diff --git a/src/engine/value/conversion/primitive.rs b/src/engine/value/conversion/primitive.rs index ccd84a06..8934d174 100644 --- a/src/engine/value/conversion/primitive.rs +++ b/src/engine/value/conversion/primitive.rs @@ -1,6 +1,7 @@ //! Owned ToPrimitive phases. A reply consumes its continuation exactly once. use super::*; -use crate::engine::object::CallableRef; +use crate::engine::object::{CallableRef, ObjectRef}; +use crate::engine::value::JsValue; pub(crate) enum PrimitiveStep { Get { resume: PrimitiveResume }, @@ -30,8 +31,8 @@ pub(crate) struct PrimitiveResumeState { requested_object: Option, requested_key: Option, requested_callable: Option, - requested_receiver: Option, - requested_arguments: Vec, + requested_receiver: Option, + requested_arguments: Vec, } enum Phase { @@ -50,8 +51,8 @@ impl PrimitiveResume { fn call( mut self, callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, ) -> PrimitiveStep { self.requested_callable = Some(callable); self.requested_receiver = Some(receiver); @@ -69,24 +70,25 @@ impl PrimitiveResume { .take() .expect("primitive call callee") } - pub(crate) fn take_receiver(&mut self) -> Value { + pub(crate) fn take_receiver(&mut self) -> JsValue { self.requested_receiver .take() .expect("primitive call receiver") } - pub(crate) fn take_arguments(&mut self) -> Vec { + pub(crate) fn take_arguments(&mut self) -> Vec { std::mem::take(&mut self.requested_arguments) } pub(crate) fn start( runtime: &Runtime, realm: ContextId, - value: Value, + value: JsValue, hint: ToPrimitiveHint, ) -> PrimitiveStep { - let Value::Object(object) = value else { + let JsValue::Object(object) = value else { return PrimitiveStep::Complete(Completion::Return(value)); }; + let object = ObjectRef::from_owned_handle(runtime.clone(), object); let key = PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::ToPrimitive)); let requested = object.clone(); Self(Box::new(PrimitiveResumeState { @@ -150,7 +152,7 @@ impl PrimitiveResume { fn type_error(self, runtime: &Runtime, message: &str) -> Result { Ok(PrimitiveStep::Complete(Completion::Throw( - runtime.new_native_error(self.0.realm, NativeErrorKind::Type, message)?, + runtime.new_native_error_jsvalue(self.0.realm, NativeErrorKind::Type, message)?, ))) } @@ -167,44 +169,55 @@ impl PrimitiveResume { }; match self.0.phase { Phase::ExoticMethod => { - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { + runtime.release_jsvalue(value)?; return self.read_ordinary(runtime, false); } - let Value::Object(method) = value else { + let JsValue::Object(method) = &value else { + runtime.release_jsvalue(value)?; return self.type_error(runtime, "not a function"); }; + let method = ObjectRef::from_borrowed_handle(runtime.clone(), *method)?; + runtime.release_jsvalue(value)?; let Some(callable) = runtime.as_callable(&method)? else { return self.type_error(runtime, "not a function"); }; - let argument = Value::String(JsString::from_static(match self.0.hint { - ToPrimitiveHint::String => "string", - ToPrimitiveHint::Number => "number", - ToPrimitiveHint::Default => "default", - })); + let argument = runtime.into_jsvalue(Value::String(JsString::from_static( + match self.0.hint { + ToPrimitiveHint::String => "string", + ToPrimitiveHint::Number => "number", + ToPrimitiveHint::Default => "default", + }, + )))?; self.0.phase = Phase::ExoticResult; - let receiver = Value::Object(self.0.object.clone()); + let receiver = JsValue::Object(self.0.object.clone().into_handle()); Ok(self.call(callable, receiver, vec![argument])) } Phase::ExoticResult => { - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { + runtime.release_jsvalue(value)?; self.type_error(runtime, "toPrimitive") } else { Ok(PrimitiveStep::Complete(Completion::Return(value))) } } Phase::OrdinaryMethod(second) => { - let Value::Object(method) = value else { + let JsValue::Object(method) = &value else { + runtime.release_jsvalue(value)?; return self.failed_method(runtime, second); }; + let method = ObjectRef::from_borrowed_handle(runtime.clone(), *method)?; + runtime.release_jsvalue(value)?; let Some(callable) = runtime.as_callable(&method)? else { return self.failed_method(runtime, second); }; self.0.phase = Phase::OrdinaryResult(second); - let receiver = Value::Object(self.0.object.clone()); + let receiver = JsValue::Object(self.0.object.clone().into_handle()); Ok(self.call(callable, receiver, Vec::new())) } Phase::OrdinaryResult(second) => { - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { + runtime.release_jsvalue(value)?; self.failed_method(runtime, second) } else { Ok(PrimitiveStep::Complete(Completion::Return(value))) @@ -232,8 +245,12 @@ impl Runtime { } PrimitiveStep::Call { mut resume } => { let callable = resume.take_callable(); - let receiver = resume.take_receiver(); - let arguments = resume.take_arguments(); + let receiver = self.root_and_release_jsvalue(resume.take_receiver())?; + let arguments = resume + .take_arguments() + .into_iter() + .map(|argument| self.root_and_release_jsvalue(argument)) + .collect::, _>>()?; let completion = self.call_internal(realm, &callable, receiver, &arguments)?; resume.resume(self, completion)? } @@ -263,6 +280,7 @@ mod resident_request_tests { let runtime = Runtime::new(); let mut context = runtime.new_context(); let value = context.eval("({valueOf(){return 7}})").unwrap(); + let value = runtime.unroot_value(&value).unwrap(); let PrimitiveStep::Get { mut resume } = PrimitiveResume::start(&runtime, context.realm, value, ToPrimitiveHint::Number) else { @@ -287,14 +305,20 @@ mod resident_request_tests { }; assert_eq!(&*resume.0 as *const PrimitiveResumeState, address); let callable = resume.take_callable(); - let receiver = resume.take_receiver(); - let arguments = resume.take_arguments(); + let receiver = runtime + .root_and_release_jsvalue(resume.take_receiver()) + .unwrap(); + let arguments = resume + .take_arguments() + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument).unwrap()) + .collect::>(); let completion = runtime .call_internal(context.realm, &callable, receiver, &arguments) .unwrap(); assert!(matches!( resume.resume(&runtime, completion).unwrap(), - PrimitiveStep::Complete(Completion::Return(Value::Int(7))) + PrimitiveStep::Complete(Completion::Return(JsValue::Int(7))) )); } } diff --git a/src/engine/value/js_value.rs b/src/engine/value/js_value.rs new file mode 100644 index 00000000..f59a1e83 --- /dev/null +++ b/src/engine/value/js_value.rs @@ -0,0 +1,757 @@ +//! Crate-internal execution value and the public-API conversion boundary. +//! +//! [`JsValue`] is the engine-internal value representation mandated by the +//! S3-A design: scalars inline, every heap-backed kind (string, BigInt, +//! symbol, object) carried as a generational typed handle. It deliberately +//! implements neither `Copy` nor `Drop`: every storage position owns its +//! handle edge explicitly, duplicated with [`Runtime::dup_jsvalue`] and +//! surrendered with [`Runtime::release_jsvalue`], mirroring QuickJS's C +//! ownership discipline. +//! +//! The public [`Value`] (with its `Rc`-rooted object/symbol +//! wrappers) stays the only type crossing the embedding boundary. The two +//! directions live here and only here: +//! +//! - `unroot` / `into_jsvalue`: public root -> internal value (entering the +//! engine). The borrowed form duplicates the heap edge; the consuming +//! form transfers the root's owned edge without a retain/release pair. +//! - `root`: internal value -> public root (leaving the engine), duplicating +//! the edge and wrapping it in the public root types. +//! +//! String and BigInt conversion allocates one arena node per conversion: +//! node allocation happens only at genuine creation points (here: API/host +//! input conversion), never at value stores. + +use crate::engine::api::runtime::Runtime; +use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::AtomIdx; +use crate::engine::heap::RawValue; +use crate::engine::heap::{BigIntId, ObjectId, StringId}; +use crate::engine::value::Value; + +/// Engine-internal value: scalars inline, heap kinds as generational handles. +/// +/// See the module documentation for the ownership contract. A `JsValue` is +/// 16 bytes (compile-time asserted below), half the public [`Value`]. +/// +/// It deliberately does not derive `PartialEq`: handle identity is not value +/// equality (two distinct string nodes can hold equal text), so equality must +/// go through the heap-aware helpers instead. +pub enum JsValue { + Undefined, + Null, + Bool(bool), + Int(i32), + Float(f64), + String(StringId), + BigInt(BigIntId), + Symbol(AtomIdx), + Object(ObjectId), +} + +const _: () = assert!(std::mem::size_of::() == 16); +const _: () = assert!(std::mem::size_of::() == 4); + +#[cfg(test)] +impl PartialEq for JsValue { + /// Test-only representation equality: scalars by value, heap kinds by + /// handle id. Production equality must go through the heap-aware helpers + /// (D2.6: id equality is not content equality), so this impl exists only + /// for unit tests that assert exact internal representations. + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Undefined, Self::Undefined) | (Self::Null, Self::Null) => true, + (Self::Bool(left), Self::Bool(right)) => left == right, + (Self::Int(left), Self::Int(right)) => left == right, + (Self::Float(left), Self::Float(right)) => left == right, + (Self::String(left), Self::String(right)) => left == right, + (Self::BigInt(left), Self::BigInt(right)) => left == right, + (Self::Symbol(left), Self::Symbol(right)) => left == right, + (Self::Object(left), Self::Object(right)) => left == right, + _ => false, + } + } +} + +impl JsValue { + /// Representation-only `typeof` tag, matching [`Value::type_of`]. + #[must_use] + pub const fn type_of(&self) -> &'static str { + match self { + Self::Null => "object", + Self::Bool(_) => "boolean", + Self::Int(_) | Self::Float(_) => "number", + Self::BigInt(_) => "bigint", + Self::String(_) => "string", + Self::Symbol(_) => "symbol", + Self::Object(_) => "object", + Self::Undefined => "undefined", + } + } + + /// Representation-only `Number` projection; never performs ToNumber. + #[must_use] + pub(crate) fn as_number_repr( + &self, + ) -> Option { + match self { + Self::Int(value) => Some(crate::engine::value::number::operations::Number::Int( + *value, + )), + Self::Float(value) => Some(crate::engine::value::number::operations::Number::Float( + *value, + )), + _ => None, + } + } + + /// Representation-only number projection as `f64`; never performs ToNumber. + #[must_use] + pub const fn as_number(&self) -> Option { + match self { + Self::Int(value) => Some(*value as f64), + Self::Float(value) => Some(*value), + _ => None, + } + } + + /// Representation-only primitive `ToBoolean` (no HTMLDDA exception). + #[must_use] + pub(crate) fn to_boolean_primitive(&self) -> bool { + match self { + Self::Bool(value) => *value, + Self::Int(value) => *value != 0, + Self::Float(value) => *value != 0.0 && !value.is_nan(), + Self::BigInt(_) | Self::String(_) => true, + Self::Symbol(_) | Self::Object(_) => true, + Self::Undefined | Self::Null => false, + } + } + + /// Borrow as the heap storage payload: the same handle ids, no allocation. + #[must_use] + pub(crate) fn as_raw(&self) -> RawValue { + match self { + Self::Undefined => RawValue::Undefined, + Self::Null => RawValue::Null, + Self::Bool(value) => RawValue::Bool(*value), + Self::Int(value) => RawValue::Int(*value), + Self::Float(value) => RawValue::Float(*value), + Self::String(id) => RawValue::String(*id), + Self::BigInt(id) => RawValue::BigInt(*id), + Self::Symbol(index) => RawValue::Symbol(*index), + Self::Object(id) => RawValue::Object(*id), + } + } + + /// Convert a heap storage payload into an internal value. + /// + /// This is a plain same-id copy of every edge the payload carries; no + /// retain happens here, so the caller must account for the edge ownership + /// of both sides. Heap-private payloads (`Private`, `Uninitialized`, + /// `Exception`) have no internal-value form and return `None`. + #[must_use] + pub(crate) fn from_raw(raw: RawValue) -> Option { + Some(match raw { + RawValue::Undefined => Self::Undefined, + RawValue::Null => Self::Null, + RawValue::Bool(value) => Self::Bool(value), + RawValue::Int(value) => Self::Int(value), + RawValue::Float(value) => Self::Float(value), + RawValue::String(id) => Self::String(id), + RawValue::BigInt(id) => Self::BigInt(id), + RawValue::Symbol(index) => Self::Symbol(index), + RawValue::Object(id) => Self::Object(id), + RawValue::Private(_) | RawValue::Uninitialized | RawValue::Exception => { + return None; + } + }) + } +} + +impl From for JsValue { + /// Project an already-compacted numeric representation. Scalars only: no + /// heap edge is created or duplicated. + fn from(value: crate::engine::value::number::operations::Number) -> Self { + match value { + crate::engine::value::number::operations::Number::Int(value) => Self::Int(value), + crate::engine::value::number::operations::Number::Float(value) => Self::Float(value), + } + } +} + +impl std::fmt::Debug for JsValue { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Undefined => formatter.write_str("JsValue::Undefined"), + Self::Null => formatter.write_str("JsValue::Null"), + Self::Bool(value) => formatter.debug_tuple("JsValue::Bool").field(value).finish(), + Self::Int(value) => formatter.debug_tuple("JsValue::Int").field(value).finish(), + Self::Float(value) => formatter + .debug_tuple("JsValue::Float") + .field(value) + .finish(), + Self::String(id) => formatter.debug_tuple("JsValue::String").field(id).finish(), + Self::BigInt(id) => formatter.debug_tuple("JsValue::BigInt").field(id).finish(), + Self::Symbol(index) => formatter + .debug_tuple("JsValue::Symbol") + .field(index) + .finish(), + Self::Object(id) => formatter.debug_tuple("JsValue::Object").field(id).finish(), + } + } +} + +impl Runtime { + /// Convert a borrowed public root into an internal value, duplicating + /// every heap edge it carries (entering-engine form). + /// + /// String and BigInt payloads allocate one arena node each: API input + /// conversion is a genuine creation point under the ownership rules. + /// + /// # Errors + /// + /// Returns [`RuntimeError::WrongRuntime`] for a foreign object/symbol + /// root, or a heap/atom error when an edge cannot be duplicated or a + /// node cannot be allocated. + pub(crate) fn unroot_value(&self, value: &Value) -> Result { + Ok(match value { + Value::Undefined => JsValue::Undefined, + Value::Null => JsValue::Null, + Value::Bool(value) => JsValue::Bool(*value), + Value::Int(value) => JsValue::Int(*value), + Value::Float(value) => JsValue::Float(*value), + Value::Object(object) => { + if !object.belongs_to(self) { + return Err(RuntimeError::WrongRuntime("object root conversion")); + } + let id = object.object_id(); + self.retain_object_handle(id)?; + JsValue::Object(id) + } + Value::Symbol(symbol) => { + if !symbol.belongs_to(self) { + return Err(RuntimeError::WrongRuntime("symbol root conversion")); + } + let atom = symbol.atom(); + self.retain_atom_handle(atom)?; + let index = self.0.state.borrow().atoms.unbrand(atom)?; + JsValue::Symbol(index) + } + Value::String(string) => { + let id = self + .0 + .state + .borrow_mut() + .heap + .allocate_string(string.clone())?; + JsValue::String(id) + } + Value::BigInt(bigint) => { + let id = self + .0 + .state + .borrow_mut() + .heap + .allocate_bigint(bigint.clone())?; + #[cfg(debug_assertions)] + if std::env::var("QJS_TRACE_BIGINT_ID") + .is_ok_and(|value| format!("{id:?}").contains(&format!("index: {value},"))) + { + eprintln!( + "[unroot-b] {id:?}\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + JsValue::BigInt(id) + } + }) + } + + /// Consume a public root into an internal value, transferring the edge + /// the root owned without a retain/release pair (entering-engine form). + /// + /// Object and symbol roots hand over their exactly-one owned reference; + /// string and BigInt payloads move into a freshly allocated arena node. + /// + /// # Errors + /// + /// Returns [`RuntimeError::WrongRuntime`] for a foreign object/symbol + /// root, or a heap/atom error when a node cannot be allocated. + // `into_` names the consumed `Value` argument; the receiver is the runtime. + #[allow(clippy::wrong_self_convention)] + pub(crate) fn into_jsvalue(&self, value: Value) -> Result { + Ok(match value { + Value::Undefined => JsValue::Undefined, + Value::Null => JsValue::Null, + Value::Bool(value) => JsValue::Bool(value), + Value::Int(value) => JsValue::Int(value), + Value::Float(value) => JsValue::Float(value), + Value::Object(object) => { + if !object.belongs_to(self) { + return Err(RuntimeError::WrongRuntime("object root conversion")); + } + JsValue::Object(object.into_handle()) + } + Value::Symbol(symbol) => { + if !symbol.belongs_to(self) { + return Err(RuntimeError::WrongRuntime("symbol root conversion")); + } + let atom = symbol.into_atom(); + let index = self.0.state.borrow().atoms.unbrand(atom)?; + JsValue::Symbol(index) + } + Value::String(string) => { + let id = self.0.state.borrow_mut().heap.allocate_string(string)?; + JsValue::String(id) + } + Value::BigInt(bigint) => { + let id = self.0.state.borrow_mut().heap.allocate_bigint(bigint)?; + #[cfg(debug_assertions)] + if std::env::var("QJS_TRACE_BIGINT_ID") + .is_ok_and(|value| format!("{id:?}").contains(&format!("index: {value},"))) + { + eprintln!( + "[into-b] {id:?}\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + JsValue::BigInt(id) + } + }) + } + + /// Convert an internal value back into a public root, duplicating every + /// heap edge it carries (leaving-engine form). + /// + /// The borrowed `JsValue` keeps its edges; the returned [`Value`] owns + /// new roots. String and BigInt payloads hand out a clone of the node's + /// inner `Rc`, preserving `JsString` identity semantics. + /// + /// # Errors + /// + /// Returns a heap/atom error when a handle no longer resolves. + pub(crate) fn root_value(&self, value: &JsValue) -> Result { + Ok(match value { + JsValue::Undefined => Value::Undefined, + JsValue::Null => Value::Null, + JsValue::Bool(value) => Value::Bool(*value), + JsValue::Int(value) => Value::Int(*value), + JsValue::Float(value) => Value::Float(*value), + JsValue::Object(id) => { + self.retain_object_handle(*id)?; + Value::Object(crate::engine::object::ObjectRef::from_owned_handle( + self.clone(), + *id, + )) + } + JsValue::Symbol(index) => { + let atom = self.0.state.borrow().atoms.brand(*index)?; + self.retain_atom_handle(atom)?; + Value::Symbol(crate::engine::object::SymbolRef::from_owned_atom( + self.clone(), + atom, + )) + } + JsValue::String(id) => { + let string = self.0.state.borrow().heap.string(*id)?.clone(); + Value::String(string) + } + JsValue::BigInt(id) => { + let bigint = self.0.state.borrow().heap.bigint(*id)?.clone(); + Value::BigInt(bigint) + } + }) + } + + /// Duplicate every heap edge carried by an internal value. + /// + /// Scalars copy; object/string/BigInt edges retain their node; symbols + /// retain their atom. The result owns independent edges and must + /// eventually be passed to [`Runtime::release_jsvalue`]. + /// + /// # Errors + /// + /// Returns a heap/atom error when an edge cannot be duplicated. + pub(crate) fn dup_jsvalue(&self, value: &JsValue) -> Result { + Ok(match value { + JsValue::Undefined => JsValue::Undefined, + JsValue::Null => JsValue::Null, + JsValue::Bool(value) => JsValue::Bool(*value), + JsValue::Int(value) => JsValue::Int(*value), + JsValue::Float(value) => JsValue::Float(*value), + JsValue::Object(id) => { + self.retain_object_handle(*id)?; + JsValue::Object(*id) + } + JsValue::String(id) => { + self.retain_string_handle(*id)?; + JsValue::String(*id) + } + JsValue::BigInt(id) => { + self.retain_bigint_handle(*id)?; + JsValue::BigInt(*id) + } + JsValue::Symbol(index) => { + let atom = self.0.state.borrow().atoms.brand(*index)?; + self.retain_atom_handle(atom)?; + JsValue::Symbol(*index) + } + }) + } + + /// Boundary adapter: root an internal value into the public representation + /// and immediately release its internal edges. The returned [`Value`] owns + /// independent roots; the consumed value surrenders every edge it carried. + /// This is the consuming form of [`Runtime::root_value`] for sub-driver + /// entry points whose callees consume public roots: the net edge count is + /// unchanged and both sides' ownership is explicit. + pub(crate) fn root_and_release_jsvalue(&self, value: JsValue) -> Result { + let rooted = self.root_value(&value)?; + self.release_jsvalue(value)?; + Ok(rooted) + } + + /// Release every heap edge carried by an internal value. + /// + /// Scalars are no-ops; heap edges take the existing deferred-release + /// path, so a release requested while the runtime state is borrowed is + /// applied at the next operation boundary. + /// + /// # Errors + /// + /// Returns a heap/atom error when an edge fails validation; releases + /// requested at trusted internal sites keep the engine-wide discipline + /// that invariant violations surface at the deferred-drain boundary. + pub(crate) fn release_jsvalue(&self, value: JsValue) -> Result<(), RuntimeError> { + match value { + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) => Ok(()), + JsValue::Object(id) => { + self.release_object_handle(id); + Ok(()) + } + JsValue::String(id) => { + self.release_string_handle(id); + Ok(()) + } + JsValue::BigInt(id) => { + self.release_bigint_handle(id); + Ok(()) + } + JsValue::Symbol(index) => { + let atom = self.0.state.borrow().atoms.brand(index)?; + self.release_atom_handle(atom); + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::atom::Atom; + use crate::engine::heap::HeapNodeKind; + use crate::engine::value::JsString; + + #[test] + fn scalars_round_trip_without_heap_edges() { + let runtime = Runtime::new(); + for value in [ + Value::Undefined, + Value::Null, + Value::Bool(true), + Value::Int(-7), + Value::Float(3.5), + ] { + let internal = runtime.unroot_value(&value).unwrap(); + assert_eq!(internal.type_of(), value.type_of()); + let rooted = runtime.root_value(&internal).unwrap(); + assert_eq!(rooted, value); + runtime.release_jsvalue(internal).unwrap(); + } + } + + #[test] + fn object_edges_dup_and_release_by_the_value() { + let runtime = Runtime::new(); + let object = runtime.new_object(None).unwrap(); + let id = object.object_id(); + let before = runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .unwrap(); + + let root = Value::Object(object); + let internal = runtime.unroot_value(&root).unwrap(); + assert_eq!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .unwrap(), + before + 1 + ); + + let dup = runtime.dup_jsvalue(&internal).unwrap(); + assert_eq!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .unwrap(), + before + 2 + ); + + let rooted = runtime.root_value(&internal).unwrap(); + assert!(matches!(rooted, Value::Object(_))); + assert_eq!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .unwrap(), + before + 3 + ); + drop(rooted); + + runtime.release_jsvalue(dup).unwrap(); + runtime.release_jsvalue(internal).unwrap(); + drop(root); + let _operation = runtime.operation(); + // The last owned edge is gone: the node was finalized and its slot + // reclaimed, so the identity now reads as stale. + assert!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .is_err() + ); + } + + #[test] + fn into_jsvalue_transfers_the_owned_edge_without_retain() { + let runtime = Runtime::new(); + let object = runtime.new_object(None).unwrap(); + let id = object.object_id(); + let before = runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .unwrap(); + + let internal = runtime.into_jsvalue(Value::Object(object)).unwrap(); + assert_eq!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .unwrap(), + before + ); + + runtime.release_jsvalue(internal).unwrap(); + let _operation = runtime.operation(); + // The transferred edge was the only one; the node is reclaimed. + assert!( + runtime + .0 + .state + .borrow() + .heap + .object_strong_count(id) + .is_err() + ); + } + + #[test] + fn foreign_roots_are_rejected() { + let runtime = Runtime::new(); + let foreign = Runtime::new(); + let object = foreign.new_object(None).unwrap(); + assert!(matches!( + runtime.unroot_value(&Value::Object(object)), + Err(RuntimeError::WrongRuntime(_)) + )); + let symbol = foreign.new_symbol(None).unwrap(); + assert!(matches!( + runtime.unroot_value(&Value::Symbol(symbol)), + Err(RuntimeError::WrongRuntime(_)) + )); + } + + #[test] + fn string_payloads_allocate_one_node_per_conversion() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let value = context.eval("'hello arena'").unwrap(); + let Value::String(string) = &value else { + panic!("eval must produce a string"); + }; + + let internal = runtime.unroot_value(&value).unwrap(); + let JsValue::String(id) = internal else { + panic!("unroot must produce a string handle"); + }; + assert_eq!(runtime.0.state.borrow().heap.string(id).unwrap(), string); + + let rooted = runtime.root_value(&internal).unwrap(); + assert_eq!(&rooted, &value); + // Reading hands out a clone of the node's inner Rc: identity is kept. + let Value::String(reread) = &rooted else { + panic!("root must produce a string"); + }; + assert!(reread.same_representation(string)); + + runtime.release_jsvalue(internal).unwrap(); + let _operation = runtime.operation(); + assert!(runtime.0.state.borrow().heap.string(id).is_err()); + } + + #[test] + fn bigint_payloads_allocate_one_node_per_conversion() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let value = context.eval("123456789012345678901234567890n").unwrap(); + let Value::BigInt(bigint) = &value else { + panic!("eval must produce a bigint"); + }; + + let internal = runtime.into_jsvalue(value.clone()).unwrap(); + let JsValue::BigInt(id) = internal else { + panic!("conversion must produce a bigint handle"); + }; + assert_eq!(runtime.0.state.borrow().heap.bigint(id).unwrap(), bigint); + + let rooted = runtime.root_value(&internal).unwrap(); + assert_eq!(&rooted, &value); + runtime.release_jsvalue(internal).unwrap(); + let _operation = runtime.operation(); + assert!(runtime.0.state.borrow().heap.bigint(id).is_err()); + } + + #[test] + fn symbol_edges_use_the_atom_table() { + let runtime = Runtime::new(); + let symbol = runtime + .new_symbol(Some(JsString::from_static("internal"))) + .unwrap(); + let atom = symbol.atom(); + let root = Value::Symbol(symbol); + + let internal = runtime.unroot_value(&root).unwrap(); + let JsValue::Symbol(index) = internal else { + panic!("unroot must produce a symbol index"); + }; + assert_eq!(index, runtime.0.state.borrow().atoms.unbrand(atom).unwrap()); + let retained = runtime + .0 + .state + .borrow() + .atoms + .resolve(atom) + .unwrap() + .ref_count; + assert_eq!(retained, Some(2)); + + let dup = runtime.dup_jsvalue(&internal).unwrap(); + let retained = runtime + .0 + .state + .borrow() + .atoms + .resolve(atom) + .unwrap() + .ref_count; + assert_eq!(retained, Some(3)); + + let rooted = runtime.root_value(&dup).unwrap(); + assert!(matches!(rooted, Value::Symbol(_))); + drop(rooted); + + runtime.release_jsvalue(dup).unwrap(); + runtime.release_jsvalue(internal).unwrap(); + let _operation = runtime.operation(); + let retained = runtime + .0 + .state + .borrow() + .atoms + .resolve(atom) + .unwrap() + .ref_count; + assert_eq!(retained, Some(1)); + drop(root); + } + + #[test] + fn stale_symbol_index_fails_boundary_branding() { + let runtime = Runtime::new(); + let atom: Atom = runtime + .0 + .state + .borrow_mut() + .atoms + .intern("transient") + .unwrap(); + let index = runtime.0.state.borrow().atoms.unbrand(atom).unwrap(); + // Release the only owner; the slot is reclaimed synchronously. + runtime.0.state.borrow_mut().atoms.release(atom).unwrap(); + assert!(runtime.0.state.borrow().atoms.brand(index).is_err()); + } + + #[test] + fn string_kind_is_reported_in_counts_and_cleanup() { + let runtime = Runtime::new(); + let value = Value::String(JsString::from_static("counted")); + let internal = runtime.unroot_value(&value).unwrap(); + let JsValue::String(id) = internal else { + panic!("unroot must produce a string handle"); + }; + assert!(runtime.0.state.borrow().heap.string(id).is_ok()); + runtime.release_jsvalue(internal).unwrap(); + let _operation = runtime.operation(); + assert!(runtime.0.state.borrow().heap.string(id).is_err()); + } + + #[test] + fn release_defers_while_state_is_borrowed() { + let runtime = Runtime::new(); + let object = runtime.new_object(None).unwrap(); + let root = Value::Object(object); + let internal = runtime.unroot_value(&root).unwrap(); + { + let _state = runtime.0.state.borrow(); + runtime.release_jsvalue(internal).unwrap(); + assert!(runtime.0.deferred_references.has_pending()); + } + let _operation = runtime.operation(); + assert!(!runtime.0.deferred_references.has_pending()); + } + + #[test] + fn node_kind_reports_string_and_bigint() { + assert_eq!(HeapNodeKind::String, HeapNodeKind::String); + assert_ne!(HeapNodeKind::String, HeapNodeKind::BigInt); + } +} diff --git a/src/engine/value/mod.rs b/src/engine/value/mod.rs index 4342978b..f60d26ff 100644 --- a/src/engine/value/mod.rs +++ b/src/engine/value/mod.rs @@ -10,6 +10,9 @@ use crate::engine::value::bigint::JsBigInt; mod primitive; pub use primitive::*; +pub(crate) mod js_value; +pub(crate) use js_value::JsValue; + #[derive(Clone, Debug)] pub enum Value { Undefined, @@ -30,15 +33,6 @@ impl Value { number::operations::Number::compact(value).into() } - /// Representation-only Number projection; never performs ToNumber. - pub(crate) fn as_number_repr(&self) -> Option { - match self { - Self::Int(value) => Some(number::operations::Number::Int(*value)), - Self::Float(value) => Some(number::operations::Number::Float(*value)), - _ => None, - } - } - /// Match QuickJS's representation-only `JSValue` comparison. This is /// narrower than JavaScript equality: heap-backed primitives must retain /// the same cell and floating-point payload bits must match exactly. diff --git a/src/engine/value/primitive.rs b/src/engine/value/primitive.rs index 77ed24cc..e502e0ca 100644 --- a/src/engine/value/primitive.rs +++ b/src/engine/value/primitive.rs @@ -25,12 +25,6 @@ pub struct JsString(Rc); pub struct WeakJsString(Weak); impl WeakJsString { - /// Weak ownership keeps the allocation identity reserved after payload - /// destruction, so address reuse cannot create a false cache hit. - pub(crate) fn same_representation(&self, string: &JsString) -> bool { - self.0.as_ptr() == Rc::as_ptr(&string.0) - } - #[must_use] pub fn upgrade(&self) -> Option { self.0.upgrade().map(JsString) diff --git a/src/engine/vm/arguments_driver.rs b/src/engine/vm/arguments_driver.rs index 32adc94a..007fc8db 100644 --- a/src/engine/vm/arguments_driver.rs +++ b/src/engine/vm/arguments_driver.rs @@ -11,14 +11,22 @@ use crate::engine::code::bytecode::ArgumentsKind; use crate::engine::code::function::metadata::{ ClosureSource, ClosureVariable, ClosureVariableKind, ClosureVariableName, }; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; + +fn root_values(runtime: &Runtime, values: Vec) -> Result, Error> { + values + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error) +} pub(super) fn arguments( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, kind: ArgumentsKind, -) -> Result { +) -> Result { let frame = execution.frames.current_mut(id)?; let count = execution.slots.actual_argument_count(&frame.window)?; let object = match kind { @@ -26,6 +34,7 @@ pub(super) fn arguments( let values = execution .slots .snapshot_actual_arguments(&frame.window, runtime)?; + let values = root_values(runtime, values)?; runtime.new_unmapped_arguments_object(frame.executable.realm, values) } ArgumentsKind::Mapped => { @@ -63,7 +72,7 @@ pub(super) fn arguments( } } .map_err(runtime_error_to_vm_error)?; - Ok(Value::Object(object)) + Ok(JsValue::Object(object.into_handle())) } pub(super) fn rest( @@ -71,15 +80,16 @@ pub(super) fn rest( execution: &mut RunningExecution, id: FrameId, start: u16, -) -> Result { +) -> Result { let frame = execution.frames.current_mut(id)?; let values = execution .slots .snapshot_argument_tail(&frame.window, runtime, usize::from(start))?; + let values = root_values(runtime, values)?; runtime .new_array_from_values(frame.executable.realm, values) - .map(Value::Object) + .map(|object| JsValue::Object(object.into_handle())) .map_err(runtime_error_to_vm_error) } @@ -121,7 +131,7 @@ pub(super) fn step( }; Ok(Some(super::Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } diff --git a/src/engine/vm/array_driver.rs b/src/engine/vm/array_driver.rs index 9b3a4c0b..a70b722e 100644 --- a/src/engine/vm/array_driver.rs +++ b/src/engine/vm/array_driver.rs @@ -17,17 +17,26 @@ pub(super) fn define_element( ) -> Result { let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; - let Value::Object(object) = execution.slots.peek(&frame.window, 2)? else { - return super::property_driver::throw_error( - runtime, - realm, - Error::new(ErrorKind::Type, "not an object"), - ); + let object = match runtime + .root_value(execution.slots.peek(&frame.window, 2)?) + .map_err(runtime_error_to_vm_error)? + { + Value::Object(object) => object, + _ => { + return super::property_driver::throw_error( + runtime, + realm, + Error::new(ErrorKind::Type, "not an object"), + ); + } }; - let object = object.clone(); - let key = execution.slots.peek(&frame.window, 1)?.clone(); + let key = runtime + .root_value(execution.slots.peek(&frame.window, 1)?) + .map_err(runtime_error_to_vm_error)?; let depth = execution.slots.depth(&frame.window); - let value = execution.slots.pop(&mut frame.window)?; + let value = runtime + .root_and_release_jsvalue(execution.slots.pop(&mut frame.window)?) + .map_err(runtime_error_to_vm_error)?; let step = match LiteralDefinitionStep::start(runtime, realm, object, key, value) { Ok(step) => step, Err(error) => { diff --git a/src/engine/vm/async_from_sync_iterator.rs b/src/engine/vm/async_from_sync_iterator.rs index d123a558..326ee502 100644 --- a/src/engine/vm/async_from_sync_iterator.rs +++ b/src/engine/vm/async_from_sync_iterator.rs @@ -34,16 +34,24 @@ impl Runtime { let async_key = PropertyKey::from(self.well_known_symbol(WellKnownSymbol::AsyncIterator)); let async_method = match self.get_value_property_in_realm(realm, iterable.clone(), &async_key)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } }; let iterator = if matches!(async_method, Value::Undefined | Value::Null) { let sync_key = PropertyKey::from(self.well_known_symbol(WellKnownSymbol::Iterator)); let sync_method = match self.get_value_property_in_realm(realm, iterable.clone(), &sync_key)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } }; let sync_method = match self.async_from_sync_callable(realm, sync_method, "not a function")? { @@ -51,21 +59,31 @@ impl Runtime { NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), }; let sync_iterator = match self.call_internal(realm, &sync_method, iterable, &[])? { - Completion::Return(Value::Object(iterator)) => iterator, - Completion::Return(_) => { - return Ok(NativeConversion::Throw(self.new_native_error( - realm, - NativeErrorKind::Type, - "not an object", - )?)); + Completion::Return(value) => match self.root_and_release_jsvalue(value)? { + Value::Object(iterator) => iterator, + _ => { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Type, + "not an object", + )?)); + } + }, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); } - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), }; let next_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?; let next = match self.get_property_in_realm(realm, &sync_iterator, &next_key)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } }; Value::Object(self.new_async_from_sync_iterator(realm, &sync_iterator, &next)?) } else { @@ -78,22 +96,32 @@ impl Runtime { NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), }; match self.call_internal(realm, &async_method, iterable, &[])? { - Completion::Return(Value::Object(iterator)) => Value::Object(iterator), - Completion::Return(_) => { - return Ok(NativeConversion::Throw(self.new_native_error( - realm, - NativeErrorKind::Type, - "not an object", - )?)); + Completion::Return(value) => match self.root_and_release_jsvalue(value)? { + Value::Object(iterator) => Value::Object(iterator), + _ => { + return Ok(NativeConversion::Throw(self.new_native_error( + realm, + NativeErrorKind::Type, + "not an object", + )?)); + } + }, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); } - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), } }; let next_key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Next)?; let next = match self.get_value_property_in_realm(realm, iterator.clone(), &next_key)? { - Completion::Return(value) => value, - Completion::Throw(value) => return Ok(NativeConversion::Throw(value)), + Completion::Return(value) => self.root_and_release_jsvalue(value)?, + Completion::Throw(value) => { + return Ok(NativeConversion::Throw( + self.root_and_release_jsvalue(value)?, + )); + } }; Ok(NativeConversion::Value((iterator, next))) } @@ -121,6 +149,60 @@ impl Runtime { Ok(NativeConversion::Value(callable)) } + /// Internal-value form of [`Runtime::new_async_from_sync_iterator`]. The + /// borrowed value already owns its edges; the wrapper retains its own copy + /// transactionally, so no producer edge exists. + pub(super) fn new_async_from_sync_iterator_jsvalue( + &self, + realm: ContextId, + sync_iterator: crate::engine::heap::ObjectId, + next: &crate::engine::value::JsValue, + ) -> Result { + let prototype = self + .0 + .state + .borrow() + .heap + .context(realm)? + .async_generator + .ok_or(RuntimeError::Invariant( + "realm has no AsyncGenerator intrinsics", + ))? + .async_from_sync_iterator_prototype; + let prototype = ObjectRef::from_borrowed_handle(self.clone(), prototype)?; + let raw_next = next.as_raw(); + let mut state = self.0.state.borrow_mut(); + let shape = state.get_or_create_shape(Some(prototype.object_id()), &[])?; + let retained_atoms = match state.retain_raw_value_atoms(std::iter::once(&raw_next)) { + Ok(atoms) => atoms, + Err(error) => { + let cleanup = state.heap.release_shape(shape)?; + state.apply_cleanup(cleanup)?; + return Err(error); + } + }; + let object = match state + .heap + .allocate_object(ObjectData::async_from_sync_iterator( + shape, + Vec::new(), + sync_iterator, + raw_next, + )) { + Ok(object) => object, + Err(error) => { + state.release_atoms(retained_atoms)?; + let cleanup = state.heap.release_shape(shape)?; + state.apply_cleanup(cleanup)?; + return Err(error.into()); + } + }; + let cleanup = state.heap.release_shape(shape)?; + state.apply_cleanup(cleanup)?; + Ok(ObjectRef::from_owned_handle(self.clone(), object)) + } + + #[cfg_attr(not(test), allow(dead_code))] pub(super) fn new_async_from_sync_iterator( &self, realm: ContextId, @@ -140,6 +222,10 @@ impl Runtime { .async_from_sync_iterator_prototype; let prototype = ObjectRef::from_borrowed_handle(self.clone(), prototype)?; let raw_next = self.raw_property_value(next)?; + // The conversion allocated a string/BigInt node with one producer + // edge; whichever arm runs, that edge is ours to release (the object + // retains its own copy edge on success). + let conversion_edge = raw_next.conversion_node_edge(); let mut state = self.0.state.borrow_mut(); let shape = state.get_or_create_shape(Some(prototype.object_id()), &[])?; let retained_atoms = match state.retain_raw_value_atoms(std::iter::once(&raw_next)) { @@ -147,6 +233,10 @@ impl Runtime { Err(error) => { let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } return Err(error); } }; @@ -163,12 +253,19 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } return Err(error.into()); } }; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } Ok(ObjectRef::from_owned_handle(self.clone(), object)) } @@ -179,14 +276,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - FromSyncStep::start( - self, - realm, - NativeFunctionId::AsyncFromSyncIteratorResume(kind), - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + FromSyncStep::start( + self, + realm, + NativeFunctionId::AsyncFromSyncIteratorResume(kind), + invocation, + arguments, + )? + .finish(self, realm) + }) } pub(crate) fn call_async_from_sync_iterator_unwrap( @@ -195,11 +294,13 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "Async-from-Sync unwrap did not receive a call invocation", )); }; + invocation.release(self)?; let active = self.active_function()?; let internal = self .0 @@ -218,13 +319,16 @@ impl Runtime { let value = arguments .readable .first() - .cloned() + .map(|value| self.dup_jsvalue(value)) + .transpose()? .ok_or(RuntimeError::Invariant( "Async-from-Sync unwrap argv was not padded", ))?; - Ok(Completion::Return(Value::Object( - self.new_iterator_result(realm, value, done)?, - ))) + let value = self.root_and_release_jsvalue(value)?; + let result = self.new_iterator_result(realm, value, done)?; + Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )) } pub(crate) fn call_async_from_sync_iterator_close( @@ -233,14 +337,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - FromSyncStep::start( - self, - realm, - NativeFunctionId::AsyncFromSyncIteratorClose, - &invocation, - arguments, - )? - .finish(self, realm) + self.dispatch_borrowed_invocation(invocation, |invocation| { + FromSyncStep::start( + self, + realm, + NativeFunctionId::AsyncFromSyncIteratorClose, + invocation, + arguments, + )? + .finish(self, realm) + }) } } diff --git a/src/engine/vm/async_from_sync_iterator/operation.rs b/src/engine/vm/async_from_sync_iterator/operation.rs index 01e11da8..14925d82 100644 --- a/src/engine/vm/async_from_sync_iterator/operation.rs +++ b/src/engine/vm/async_from_sync_iterator/operation.rs @@ -6,11 +6,24 @@ use crate::engine::builtins::{ }; use crate::engine::heap::{ContextId, HeapError, InternalCallableData}; use crate::engine::object::{CallableRef, ObjectRef, PropertyKey}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, }; + +/// Reconstruct one owned internal value from the dormant wrapper record, +/// retaining every edge so the decoded value owns them independently. +fn decode_raw_jsvalue( + runtime: &Runtime, + raw: &crate::engine::heap::RawValue, +) -> Result { + let value = JsValue::from_raw(raw.clone()).ok_or(RuntimeError::Invariant( + "Async-from-Sync wrapper held an internal-only sentinel", + ))?; + runtime.dup_jsvalue(&value) +} + pub(crate) enum FromSyncStep { Complete(Completion), Read { resume: Box }, @@ -38,7 +51,7 @@ struct State { capability: RootedPromiseCapability, iterator: ObjectRef, kind: GeneratorResumeKind, - arguments: Vec, + arguments: Vec, } fn continuation(realm: ContextId, phase: Phase) -> Box { Box::new(FromSyncResume { @@ -48,17 +61,18 @@ fn continuation(realm: ContextId, phase: Phase) -> Box { }) } fn settle( + _runtime: &Runtime, realm: ContextId, capability: RootedPromiseCapability, completion: Completion, -) -> FromSyncStep { +) -> Result { let (callable, value) = match completion { Completion::Return(value) => (capability.resolve, value), Completion::Throw(value) => (capability.reject, value), }; - { + Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Undefined; + let __pending_field_receiver = JsValue::Undefined; let __pending_field_arguments = vec![value]; let __pending_field_resume = continuation(realm, Phase::Settled(capability.promise)); FromSyncStep::request_call( @@ -67,7 +81,7 @@ fn settle( __pending_field_arguments, __pending_field_resume, ) - } + }) } impl FromSyncStep { pub(crate) fn start( @@ -79,7 +93,7 @@ impl FromSyncStep { ) -> Result { if target == NativeFunctionId::AsyncFromSyncIteratorUnwrap { return runtime - .call_async_from_sync_iterator_unwrap(realm, invocation.clone(), arguments) + .call_async_from_sync_iterator_unwrap(realm, invocation.dup(runtime)?, arguments) .map(Self::Complete); } if target == NativeFunctionId::AsyncFromSyncIteratorClose { @@ -107,7 +121,8 @@ impl FromSyncStep { let reason = arguments .readable .first() - .cloned() + .map(|value| runtime.dup_jsvalue(value)) + .transpose()? .ok_or(RuntimeError::Invariant( "Async-from-Sync close argv was not padded", ))?; @@ -133,15 +148,22 @@ impl FromSyncStep { "Async-from-Sync resume did not receive a call invocation", )); }; - let argument = arguments - .readable - .first() - .cloned() - .ok_or(RuntimeError::Invariant( - "Async-from-Sync resume argv was not padded", - ))?; - let receiver = if let Value::Object(receiver) = this_value { - Some(receiver) + let argument = if arguments.actual_arg_count == 0 { + None + } else { + Some( + arguments + .readable + .first() + .map(|value| runtime.dup_jsvalue(value)) + .transpose()? + .ok_or(RuntimeError::Invariant( + "Async-from-Sync resume argv was not padded", + ))?, + ) + }; + let receiver = if let JsValue::Object(receiver) = this_value { + Some(ObjectRef::from_borrowed_handle(runtime.clone(), *receiver)?) } else { None }; @@ -157,12 +179,12 @@ impl FromSyncStep { let (iterator, cached_next) = match state { Ok(state) => state, Err(HeapError::Invariant(_)) => { - let reason = runtime.new_native_error( + let reason = runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an Async-from-Sync Iterator", )?; - return Ok(settle(realm, capability, Completion::Throw(reason))); + return settle(runtime, realm, capability, Completion::Throw(reason)); } Err(error) => return Err(error.into()), }; @@ -171,19 +193,19 @@ impl FromSyncStep { capability, iterator, kind, - arguments: if arguments.actual_arg_count == 0 { - Vec::new() - } else { - vec![argument] + arguments: match argument { + Some(argument) => vec![argument], + None => Vec::new(), }, }; match kind { GeneratorResumeKind::Next => continuation(realm, Phase::Method(state)).resume( runtime, - Completion::Return(runtime.root_raw_value(&cached_next)?), + Completion::Return(decode_raw_jsvalue(runtime, &cached_next)?), ), GeneratorResumeKind::Return | GeneratorResumeKind::Throw => Ok({ - let __pending_field_receiver = Value::Object(state.iterator.clone()); + let __pending_field_receiver = + runtime.into_jsvalue(Value::Object(state.iterator.clone()))?; let __pending_field_key = runtime.intern_property_key(if kind == GeneratorResumeKind::Return { "return" @@ -228,7 +250,7 @@ impl FromSyncResume { if let Phase::Settled(promise) = phase { return match completion { Completion::Return(_) => Ok(FromSyncStep::Complete(Completion::Return( - Value::Object(promise), + runtime.into_jsvalue(Value::Object(promise))?, ))), Completion::Throw(_) => Err(RuntimeError::Invariant( "intrinsic Promise resolving function threw", @@ -257,31 +279,36 @@ impl FromSyncResume { | Phase::Done { state, .. } | Phase::Value { state, .. } | Phase::Promise { state, .. } => { - self.settle(state.capability, Completion::Throw(reason)) + self.settle(runtime, state.capability, Completion::Throw(reason))? } Phase::MissingThrow(capability) | Phase::Reject(capability) => { - self.settle(capability, Completion::Throw(reason)) + self.settle(runtime, capability, Completion::Throw(reason))? } Phase::Identity | Phase::Settled(_) => unreachable!(), }); } }; match phase { - Phase::Method(state) => { - if matches!(value, Value::Undefined | Value::Null) { + Phase::Method(mut state) => { + if matches!(value, JsValue::Undefined | JsValue::Null) { return Ok(match state.kind { GeneratorResumeKind::Return => { let value = state .arguments .into_iter() .next() - .unwrap_or(Value::Undefined); + .unwrap_or(JsValue::Undefined); + let value = runtime.root_and_release_jsvalue(value)?; let result = runtime.new_iterator_result(realm, value, true)?; - self.settle(state.capability, Completion::Return(Value::Object(result))) + self.settle( + runtime, + state.capability, + Completion::Return(runtime.into_jsvalue(Value::Object(result))?), + )? } GeneratorResumeKind::Throw => { let __pending_field_iterator = state.iterator; - let __pending_field_completion = Completion::Return(Value::Undefined); + let __pending_field_completion = Completion::Return(JsValue::Undefined); let __pending_field_resume = self.continue_with(Phase::MissingThrow(state.capability)); FromSyncStep::request_close( @@ -291,26 +318,32 @@ impl FromSyncResume { ) } GeneratorResumeKind::Next => { - let reason = runtime.new_native_error( + let reason = runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not a function", )?; - self.settle(state.capability, Completion::Throw(reason)) + self.settle(runtime, state.capability, Completion::Throw(reason))? } }); } + let value = runtime.root_and_release_jsvalue(value)?; let callable = match runtime.async_from_sync_callable(realm, value, "not a function")? { NativeConversion::Value(callable) => callable, NativeConversion::Throw(reason) => { - return Ok(self.settle(state.capability, Completion::Throw(reason))); + return self.settle( + runtime, + state.capability, + Completion::Throw(runtime.into_jsvalue(reason)?), + ); } }; Ok({ let __pending_field_callable = callable; - let __pending_field_receiver = Value::Object(state.iterator.clone()); - let __pending_field_arguments = state.arguments.clone(); + let __pending_field_receiver = + runtime.into_jsvalue(Value::Object(state.iterator.clone()))?; + let __pending_field_arguments = std::mem::take(&mut state.arguments); let __pending_field_resume = self.continue_with(Phase::Result(state)); FromSyncStep::request_call( __pending_field_callable, @@ -321,16 +354,17 @@ impl FromSyncResume { }) } Phase::Result(state) => { - let Value::Object(result) = value else { - let reason = runtime.new_native_error( + let Value::Object(result) = runtime.root_and_release_jsvalue(value)? else { + let reason = runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "iterator must return an object", )?; - return Ok(self.settle(state.capability, Completion::Throw(reason))); + return self.settle(runtime, state.capability, Completion::Throw(reason)); }; Ok({ - let __pending_field_receiver = Value::Object(result.clone()); + let __pending_field_receiver = + runtime.into_jsvalue(Value::Object(result.clone()))?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Done)?; let __pending_field_resume = self.continue_with(Phase::Done { state, result }); @@ -342,9 +376,9 @@ impl FromSyncResume { }) } Phase::Done { state, result } => { - let done = runtime.value_to_boolean(&value)?; + let done = runtime.value_to_boolean_jsvalue(&value)?; Ok({ - let __pending_field_receiver = Value::Object(result); + let __pending_field_receiver = runtime.into_jsvalue(Value::Object(result))?; let __pending_field_key = runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Value)?; let __pending_field_resume = self.continue_with(Phase::Value { state, done }); @@ -366,7 +400,7 @@ impl FromSyncResume { ) }), Phase::Promise { state, done } => { - let Value::Object(promise) = value else { + let Value::Object(promise) = runtime.root_and_release_jsvalue(value)? else { return Err(RuntimeError::Invariant( "intrinsic PromiseResolve returned a non-object", )); @@ -398,17 +432,17 @@ impl FromSyncResume { close.as_ref(), &state.capability, )?; - Ok(FromSyncStep::Complete(Completion::Return(Value::Object( - state.capability.promise, - )))) + Ok(FromSyncStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(state.capability.promise))?, + ))) } Phase::MissingThrow(capability) => { - let reason = runtime.new_native_error( + let reason = runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "throw is not a method", )?; - Ok(self.settle(capability, Completion::Throw(reason))) + self.settle(runtime, capability, Completion::Throw(reason)) } Phase::Reject(_) | Phase::Identity | Phase::Settled(_) => { Err(RuntimeError::Invariant("Async-from-Sync unexpected reply")) @@ -423,37 +457,38 @@ impl FromSyncResume { } fn settle( self: Box, + _runtime: &Runtime, capability: RootedPromiseCapability, completion: Completion, - ) -> FromSyncStep { + ) -> Result { let (callable, value) = match completion { Completion::Return(value) => (capability.resolve, value), Completion::Throw(value) => (capability.reject, value), }; - FromSyncStep::request_call( + Ok(FromSyncStep::request_call( callable, - Value::Undefined, + JsValue::Undefined, vec![value], self.continue_with(Phase::Settled(capability.promise)), - ) + )) } } #[derive(Default)] struct FromSyncStepPending { - read_receiver: Option, + read_receiver: Option, read_key: Option, call_callable: Option, - call_receiver: Option, - call_arguments: Option>, - resolve_value: Option, + call_receiver: Option, + call_arguments: Option>, + resolve_value: Option, resolve_realm: Option, close_iterator: Option, close_completion: Option, } impl FromSyncStep { pub(crate) fn request_read( - receiver: Value, + receiver: JsValue, key: PropertyKey, mut resume: Box, ) -> Self { @@ -463,8 +498,8 @@ impl FromSyncStep { } pub(crate) fn request_call( callable: CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, mut resume: Box, ) -> Self { resume.pending_effect.call_callable = Some(callable); @@ -473,7 +508,7 @@ impl FromSyncStep { Self::Call { resume } } pub(crate) fn request_resolve( - value: Value, + value: JsValue, realm: ContextId, mut resume: Box, ) -> Self { @@ -492,7 +527,7 @@ impl FromSyncStep { } } impl FromSyncResume { - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.pending_effect .read_receiver .take() @@ -510,19 +545,19 @@ impl FromSyncResume { .take() .expect("FromSyncStep Call callable") } - pub(crate) fn take_call_receiver(&mut self) -> Value { + pub(crate) fn take_call_receiver(&mut self) -> JsValue { self.pending_effect .call_receiver .take() .expect("FromSyncStep Call receiver") } - pub(crate) fn take_call_arguments(&mut self) -> Vec { + pub(crate) fn take_call_arguments(&mut self) -> Vec { self.pending_effect .call_arguments .take() .expect("FromSyncStep Call arguments") } - pub(crate) fn take_resolve_value(&mut self) -> Value { + pub(crate) fn take_resolve_value(&mut self) -> JsValue { self.pending_effect .resolve_value .take() diff --git a/src/engine/vm/async_function.rs b/src/engine/vm/async_function.rs index 6626afcd..4e2e1db0 100644 --- a/src/engine/vm/async_function.rs +++ b/src/engine/vm/async_function.rs @@ -41,10 +41,15 @@ impl Runtime { &self, caller_realm: ContextId, ) -> Result { - let reason = - self.new_native_error(caller_realm, NativeErrorKind::Internal, "stack overflow")?; + let reason = self.new_native_error_jsvalue( + caller_realm, + NativeErrorKind::Internal, + "stack overflow", + )?; let promise = self.new_rejected_default_promise(caller_realm, reason)?; - Ok(Completion::Return(Value::Object(promise))) + Ok(Completion::Return( + self.into_jsvalue(Value::Object(promise))?, + )) } pub(crate) fn initialize_async_function_intrinsic( @@ -173,14 +178,18 @@ impl Runtime { fn store_async_function_activation( &self, state_object: &ObjectRef, - activation: &EncodedVmActivation, + activation: &mut EncodedVmActivation, ) -> Result<(), RuntimeError> { - let atoms = activation.atoms(); + let atoms = { + let state = self.0.state.borrow(); + activation.atoms(&state.atoms)? + }; let mut state = self.0.state.borrow_mut(); let mut retained_atoms = Vec::with_capacity(atoms.len()); for atom in atoms { if let Err(error) = state.atoms.retain(atom) { state.release_atoms(retained_atoms)?; + activation.release_conversion_edges(self); return Err(error.into()); } retained_atoms.push(atom); @@ -190,8 +199,12 @@ impl Runtime { .suspend_async_function(state_object.object_id(), activation.data.clone()) { state.release_atoms(retained_atoms)?; + activation.release_conversion_edges(self); return Err(error.into()); } + // The heap record retained its own activation edges, so the + // caller-owned conversion edges can drop. + activation.release_conversion_edges(self); Ok(()) } @@ -222,15 +235,18 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - let NativeInvocation::Call { .. } = invocation else { + let NativeInvocation::Call { .. } = &invocation else { + let _ = invocation.release(self); return Err(RuntimeError::Invariant( "AsyncFunction resume callback received a constructor invocation", )); }; + invocation.release(self)?; let argument = arguments .readable .first() - .cloned() + .map(|value| self.dup_jsvalue(value)) + .transpose()? .ok_or(RuntimeError::Invariant( "AsyncFunction resume callback argv was not padded", ))?; diff --git a/src/engine/vm/async_function/operation.rs b/src/engine/vm/async_function/operation.rs index ab87b5c1..71e91dc2 100644 --- a/src/engine/vm/async_function/operation.rs +++ b/src/engine/vm/async_function/operation.rs @@ -5,7 +5,7 @@ use crate::engine::heap::{ AsyncFunctionPhase, AsyncFunctionResumeKind, ContextId, InternalCallableData, }; use crate::engine::object::{CallableRef, ObjectRef}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::{ Completion, VmSuspendKind, suspend::{EncodedVmActivation, RootedVmActivation, VmActivationResume, VmRunOutcome}, @@ -22,7 +22,7 @@ pub(crate) struct AsyncResume { pending_effect: AsyncStepPending, runtime: Runtime, state: ObjectRef, - output: Value, + output: JsValue, phase: Phase, active: bool, } @@ -43,7 +43,7 @@ impl AsyncResume { pending_effect: AsyncStepPending::default(), runtime: runtime.clone(), state, - output: Value::Object(capability.promise), + output: runtime.into_jsvalue(Value::Object(capability.promise))?, phase: Phase::Body, active: true, })) @@ -53,7 +53,7 @@ impl AsyncResume { pending_effect: AsyncStepPending::default(), runtime: runtime.clone(), state, - output: Value::Undefined, + output: JsValue::Undefined, phase: Phase::Body, active: true, }) @@ -142,14 +142,18 @@ impl AsyncResume { match std::mem::replace(&mut self.phase, Phase::Body) { Phase::Body => self.body(VmRunOutcome::Complete(completion)), Phase::Settled => self.finish(), // Consume either JS completion from the internal resolving pair. - Phase::Await(activation) => { + Phase::Await(mut activation) => { let promise = match completion { Completion::Throw(reason) => return self.settle(Completion::Throw(reason)), - Completion::Return(Value::Object(promise)) => promise, - Completion::Return(_) => { - return Err(RuntimeError::Invariant( - "intrinsic PromiseResolve returned a non-object", - )); + Completion::Return(value) => { + let Value::Object(promise) = + self.runtime.root_and_release_jsvalue(value)? + else { + return Err(RuntimeError::Invariant( + "intrinsic PromiseResolve returned a non-object", + )); + }; + promise } }; let realm = self @@ -175,7 +179,7 @@ impl AsyncResume { let fulfill = make_resume(AsyncFunctionResumeKind::Fulfill)?; let reject = make_resume(AsyncFunctionResumeKind::Reject)?; self.runtime - .store_async_function_activation(&self.state, &activation)?; + .store_async_function_activation(&self.state, &mut activation)?; self.runtime .perform_promise_then_without_capability(realm, &promise, &fulfill, &reject)?; self.active = false; @@ -183,11 +187,11 @@ impl AsyncResume { } } } + // Consume the suspended resume box here, keeping its payload out of the step transport. + #[allow(clippy::boxed_local)] fn finish(mut self: Box) -> Result { - Ok(AsyncStep::Complete(Completion::Return(std::mem::replace( - &mut self.output, - Value::Undefined, - )))) + let output = std::mem::replace(&mut self.output, JsValue::Undefined); + Ok(AsyncStep::Complete(Completion::Return(output))) } } impl Drop for AsyncResume { @@ -248,10 +252,10 @@ mod tests { struct AsyncStepPending { run_activation: Option>, run_input: Option, - resolve_value: Option, + resolve_value: Option, resolve_realm: Option, call_callable: Option, - call_value: Option, + call_value: Option, } impl AsyncStep { pub(crate) fn request_run( @@ -264,7 +268,7 @@ impl AsyncStep { Self::Run { resume } } pub(crate) fn request_resolve( - value: Value, + value: JsValue, realm: ContextId, mut resume: Box, ) -> Self { @@ -274,7 +278,7 @@ impl AsyncStep { } pub(crate) fn request_call( callable: CallableRef, - value: Value, + value: JsValue, mut resume: Box, ) -> Self { resume.pending_effect.call_callable = Some(callable); @@ -295,7 +299,7 @@ impl AsyncResume { .take() .expect("AsyncStep Run input") } - pub(crate) fn take_resolve_value(&mut self) -> Value { + pub(crate) fn take_resolve_value(&mut self) -> JsValue { self.pending_effect .resolve_value .take() @@ -313,7 +317,7 @@ impl AsyncResume { .take() .expect("AsyncStep Call callable") } - pub(crate) fn take_call_value(&mut self) -> Value { + pub(crate) fn take_call_value(&mut self) -> JsValue { self.pending_effect .call_value .take() diff --git a/src/engine/vm/async_generator.rs b/src/engine/vm/async_generator.rs index 692455b4..8d83cc76 100644 --- a/src/engine/vm/async_generator.rs +++ b/src/engine/vm/async_generator.rs @@ -175,9 +175,12 @@ impl Runtime { pub(super) fn allocate_async_generator_object( &self, prototype: &ObjectRef, - activation: EncodedVmActivation, + mut activation: EncodedVmActivation, ) -> Result { - let atoms = activation.atoms(); + let atoms = { + let state = self.0.state.borrow(); + activation.atoms(&state.atoms)? + }; let mut state = self.0.state.borrow_mut(); let shape = state.get_or_create_shape(Some(prototype.object_id()), &[])?; let mut retained_atoms = Vec::with_capacity(atoms.len()); @@ -186,6 +189,7 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + activation.release_conversion_edges(self); return Err(error.into()); } retained_atoms.push(atom); @@ -200,11 +204,15 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + activation.release_conversion_edges(self); return Err(error.into()); } }; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + // The async-generator object retained its own activation edges, so + // the caller-owned conversion edges can drop. + activation.release_conversion_edges(self); drop(state); drop(activation); Ok(ObjectRef::from_owned_handle(self.clone(), object)) @@ -217,14 +225,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - AsyncGeneratorStep::start( - self, - realm, - NativeFunctionId::AsyncGeneratorPrototypeResume(kind), - &invocation, - arguments, - )? - .finish(self) + self.dispatch_borrowed_invocation(invocation, |invocation| { + AsyncGeneratorStep::start( + self, + realm, + NativeFunctionId::AsyncGeneratorPrototypeResume(kind), + invocation, + arguments, + )? + .finish(self) + }) } fn enqueue_async_generator_request( @@ -249,9 +259,12 @@ impl Runtime { .heap .async_generator_enqueue(generator.object_id(), request) { + self.release_converted_value_edge(&result); state.release_atoms(retained_atoms)?; return Err(error.into()); } + // The queued request retained its own copy of the value edge. + self.release_converted_value_edge(&result); Ok(()) } @@ -260,14 +273,18 @@ impl Runtime { generator: &ObjectRef, generator_state: AsyncGeneratorState, resume_realm: Option, - activation: &EncodedVmActivation, + activation: &mut EncodedVmActivation, ) -> Result<(), RuntimeError> { - let atoms = activation.atoms(); + let atoms = { + let state = self.0.state.borrow(); + activation.atoms(&state.atoms)? + }; let mut state = self.0.state.borrow_mut(); let mut retained_atoms = Vec::with_capacity(atoms.len()); for atom in atoms { if let Err(error) = state.atoms.retain(atom) { state.release_atoms(retained_atoms)?; + activation.release_conversion_edges(self); return Err(error.into()); } retained_atoms.push(atom); @@ -279,8 +296,12 @@ impl Runtime { resume_realm, ) { state.release_atoms(retained_atoms)?; + activation.release_conversion_edges(self); return Err(error.into()); } + // The heap record retained its own activation edges, so the + // caller-owned conversion edges can drop. + activation.release_conversion_edges(self); Ok(()) } @@ -309,14 +330,16 @@ impl Runtime { invocation: NativeInvocation, arguments: &NativeArguments, ) -> Result { - AsyncGeneratorStep::start( - self, - realm, - NativeFunctionId::AsyncGeneratorResume(target_kind), - &invocation, - arguments, - )? - .finish(self) + self.dispatch_borrowed_invocation(invocation, |invocation| { + AsyncGeneratorStep::start( + self, + realm, + NativeFunctionId::AsyncGeneratorResume(target_kind), + invocation, + arguments, + )? + .finish(self) + }) } fn root_front_async_generator_request( diff --git a/src/engine/vm/async_generator/operation.rs b/src/engine/vm/async_generator/operation.rs index 2ba873e9..39f35131 100644 --- a/src/engine/vm/async_generator/operation.rs +++ b/src/engine/vm/async_generator/operation.rs @@ -7,7 +7,7 @@ use crate::engine::heap::{ AsyncGeneratorResumeKind, AsyncGeneratorState, ContextId, InternalCallableData, ObjectPayload, }; use crate::engine::object::{CallableRef, ObjectRef}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::{ Completion, call::{NativeArguments, NativeInvocation}, @@ -29,7 +29,7 @@ pub(crate) struct AsyncGeneratorResume { runtime: Runtime, realm: ContextId, generator: Option, - output: Value, + output: JsValue, phase: Phase, cleanup: Cleanup, } @@ -78,27 +78,25 @@ impl AsyncGeneratorStep { let argument = arguments .readable .first() - .cloned() + .map(|value| runtime.dup_jsvalue(value)) + .transpose()? .ok_or(RuntimeError::Invariant( "AsyncGenerator operation argv was not padded", ))?; if let NativeFunctionId::AsyncGeneratorPrototypeResume(kind) = target { let capability = runtime.new_default_promise_capability(realm)?; - let promise = Value::Object(capability.promise.clone()); + let promise = runtime.into_jsvalue(Value::Object(capability.promise.clone()))?; let generator = match this_value { - Value::Object(generator) + JsValue::Object(generator) if matches!( - runtime - .0 - .state - .borrow() - .heap - .object(generator.object_id())? - .payload, + runtime.0.state.borrow().heap.object(*generator)?.payload, ObjectPayload::AsyncGenerator(_) ) => { - Some(generator.clone()) + Some(ObjectRef::from_borrowed_handle( + runtime.clone(), + *generator, + )?) } _ => None, }; @@ -112,7 +110,7 @@ impl AsyncGeneratorStep { cleanup: Cleanup::None, }); let Some(generator) = &resume.generator else { - let reason = runtime.new_native_error( + let reason = runtime.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not an async generator", @@ -128,7 +126,12 @@ impl AsyncGeneratorStep { ) }); }; - runtime.enqueue_async_generator_request(generator, kind, argument, &capability)?; + runtime.enqueue_async_generator_request( + generator, + kind, + runtime.root_and_release_jsvalue(argument)?, + &capability, + )?; let state = runtime .0 .state @@ -140,7 +143,7 @@ impl AsyncGeneratorStep { state, AsyncGeneratorState::Executing | AsyncGeneratorState::AwaitingReturn ) { - return Ok(resume.finish()); + return resume.finish(); } return resume.pump(); } @@ -179,7 +182,7 @@ impl AsyncGeneratorStep { runtime: runtime.clone(), realm, generator: Some(generator.clone()), - output: Value::Undefined, + output: JsValue::Undefined, phase: Phase::Body, cleanup: Cleanup::None, }); @@ -187,7 +190,7 @@ impl AsyncGeneratorStep { AsyncGeneratorResumeKind::AwaitFulfill | AsyncGeneratorResumeKind::AwaitReject => { // A synchronous resolver reentry can make a queued reaction stale. if snapshot.state != AsyncGeneratorState::Executing { - return Ok(resume.finish()); + return resume.finish(); } if snapshot.resume_realm.is_none() { return Err(RuntimeError::Invariant( @@ -240,11 +243,11 @@ impl AsyncGeneratorStep { runtime.finish_async_generator_completed_return(&generator)?; let settlement = if kind == AsyncGeneratorResumeKind::ReturnFulfill { AsyncGeneratorSettlement::Resolve { - value: argument, + value: runtime.root_and_release_jsvalue(argument)?, done: true, } } else { - AsyncGeneratorSettlement::Reject(argument) + AsyncGeneratorSettlement::Reject(runtime.root_and_release_jsvalue(argument)?) }; // Completed-return reactions service exactly one request. resume.settle(settlement, false) @@ -280,12 +283,12 @@ impl AsyncGeneratorResume { "async generator operation has no generator", )) } - fn finish(mut self: Box) -> AsyncGeneratorStep { + // Consume the suspended resume box here, keeping its payload out of the step transport. + #[allow(clippy::boxed_local)] + fn finish(mut self: Box) -> Result { self.cleanup = Cleanup::None; - AsyncGeneratorStep::Complete(Completion::Return(std::mem::replace( - &mut self.output, - Value::Undefined, - ))) + let output = std::mem::replace(&mut self.output, JsValue::Undefined); + Ok(AsyncGeneratorStep::Complete(Completion::Return(output))) } fn detach(&mut self, expected: AsyncGeneratorState) -> Result<(), RuntimeError> { let id = self.generator()?.object_id(); @@ -318,11 +321,11 @@ impl AsyncGeneratorResume { .heap .async_generator_snapshot(generator.object_id())?; let Some(request) = snapshot.queue.front() else { - return Ok(self.finish()); + return self.finish(); }; let previous = snapshot.state; match previous { - AsyncGeneratorState::AwaitingReturn => return Ok(self.finish()), + AsyncGeneratorState::AwaitingReturn => return self.finish(), AsyncGeneratorState::SuspendedStart if request.completion != GeneratorResumeKind::Next => { @@ -360,7 +363,8 @@ impl AsyncGeneratorResume { self.cleanup = Cleanup::AwaitingReturn; self.phase = Phase::CompletedReturn; Ok({ - let __pending_field_value = request.result; + let __pending_field_value = + self.runtime.into_jsvalue(request.result)?; let __pending_field_realm = self.realm; let __pending_field_resume = self; AsyncGeneratorStep::request_resolve( @@ -398,18 +402,19 @@ impl AsyncGeneratorResume { .runtime .root_front_async_generator_request(&generator)?; self.detach(previous)?; + let result = self.runtime.into_jsvalue(request.result)?; let input = match previous { AsyncGeneratorState::SuspendedStart => VmActivationResume::Initial, AsyncGeneratorState::SuspendedYield | AsyncGeneratorState::SuspendedYieldStar => { VmActivationResume::Generator(match request.completion { - GeneratorResumeKind::Next => VmResume::Next(request.result), - GeneratorResumeKind::Return => VmResume::Return(request.result), - GeneratorResumeKind::Throw => VmResume::Throw(request.result), + GeneratorResumeKind::Next => VmResume::Next(result), + GeneratorResumeKind::Return => VmResume::Return(result), + GeneratorResumeKind::Throw => VmResume::Throw(result), }) } // The still-running outer pump resumes a reentrantly parked await. AsyncGeneratorState::Executing => { - VmActivationResume::AwaitFulfill(Value::Undefined) + VmActivationResume::AwaitFulfill(JsValue::Undefined) } _ => unreachable!(), }; @@ -435,9 +440,13 @@ impl AsyncGeneratorResume { let (callable, value) = match settlement { AsyncGeneratorSettlement::Resolve { value, done } => ( request.resolve, - Value::Object(self.runtime.new_iterator_result(self.realm, value, done)?), + self.runtime.into_jsvalue(Value::Object( + self.runtime.new_iterator_result(self.realm, value, done)?, + ))?, ), - AsyncGeneratorSettlement::Reject(reason) => (request.reject, reason), + AsyncGeneratorSettlement::Reject(reason) => { + (request.reject, self.runtime.into_jsvalue(reason)?) + } }; // Allocate and root the result before transferring the queued capability. self.runtime @@ -468,14 +477,20 @@ impl AsyncGeneratorResume { self.runtime.complete_async_generator(self.generator()?)?; self.cleanup = Cleanup::None; let settlement = match completion { - Completion::Return(value) => { - AsyncGeneratorSettlement::Resolve { value, done: true } - } - Completion::Throw(value) => AsyncGeneratorSettlement::Reject(value), + Completion::Return(value) => AsyncGeneratorSettlement::Resolve { + value: self.runtime.root_and_release_jsvalue(value)?, + done: true, + }, + Completion::Throw(value) => AsyncGeneratorSettlement::Reject( + self.runtime.root_and_release_jsvalue(value)?, + ), }; self.settle(settlement, true) } - VmRunOutcome::Suspend { value, activation } => match activation.kind { + VmRunOutcome::Suspend { + value, + mut activation, + } => match activation.kind { VmSuspendKind::Yield | VmSuspendKind::AsyncYieldStar => { let state = if activation.kind == VmSuspendKind::Yield { AsyncGeneratorState::SuspendedYield @@ -486,11 +501,12 @@ impl AsyncGeneratorResume { self.generator()?, state, None, - &activation, + &mut activation, )?; self.cleanup = Cleanup::None; // Keep the encoded owner alive through the raw-edge publication. drop(activation); + let value = self.runtime.root_and_release_jsvalue(value)?; self.settle( AsyncGeneratorSettlement::Resolve { value, done: false }, true, @@ -531,16 +547,20 @@ impl AsyncGeneratorResume { if pump { self.pump() } else { - Ok(self.finish()) + self.finish() } } - Phase::Await(activation) => { + Phase::Await(mut activation) => { let promise = match completion { - Completion::Return(Value::Object(promise)) => promise, - Completion::Return(_) => { - return Err(RuntimeError::Invariant( - "intrinsic PromiseResolve returned a non-object", - )); + Completion::Return(value) => { + let Value::Object(promise) = + self.runtime.root_and_release_jsvalue(value)? + else { + return Err(RuntimeError::Invariant( + "intrinsic PromiseResolve returned a non-object", + )); + }; + promise } Completion::Throw(reason) => { let rooted = suspend::thaw( @@ -578,21 +598,25 @@ impl AsyncGeneratorResume { generator, AsyncGeneratorState::Executing, Some(self.realm), - &activation, + &mut activation, )?; self.runtime.perform_promise_then_without_capability( self.realm, &promise, &fulfill, &reject, )?; drop(activation); - Ok(self.finish()) + self.finish() } Phase::CompletedReturn => { let promise = match completion { - Completion::Return(Value::Object(promise)) => promise, - Completion::Return(_) => { - return Err(RuntimeError::Invariant( - "completed-return PromiseResolve returned a non-object", - )); + Completion::Return(value) => { + let Value::Object(promise) = + self.runtime.root_and_release_jsvalue(value)? + else { + return Err(RuntimeError::Invariant( + "completed-return PromiseResolve returned a non-object", + )); + }; + promise } Completion::Throw(reason) => self .runtime @@ -612,7 +636,7 @@ impl AsyncGeneratorResume { self.runtime.perform_promise_then_without_capability( self.realm, &promise, &fulfill, &reject, )?; - Ok(self.finish()) + self.finish() } } } @@ -622,10 +646,10 @@ impl AsyncGeneratorResume { struct AsyncGeneratorStepPending { run_activation: Option>, run_input: Option, - resolve_value: Option, + resolve_value: Option, resolve_realm: Option, call_callable: Option, - call_value: Option, + call_value: Option, } impl AsyncGeneratorStep { pub(crate) fn request_run( @@ -638,7 +662,7 @@ impl AsyncGeneratorStep { Self::Run { resume } } pub(crate) fn request_resolve( - value: Value, + value: JsValue, realm: ContextId, mut resume: Box, ) -> Self { @@ -648,7 +672,7 @@ impl AsyncGeneratorStep { } pub(crate) fn request_call( callable: CallableRef, - value: Value, + value: JsValue, mut resume: Box, ) -> Self { resume.pending_effect.call_callable = Some(callable); @@ -669,7 +693,7 @@ impl AsyncGeneratorResume { .take() .expect("AsyncGeneratorStep Run input") } - pub(crate) fn take_resolve_value(&mut self) -> Value { + pub(crate) fn take_resolve_value(&mut self) -> JsValue { self.pending_effect .resolve_value .take() @@ -687,7 +711,7 @@ impl AsyncGeneratorResume { .take() .expect("AsyncGeneratorStep Call callable") } - pub(crate) fn take_call_value(&mut self) -> Value { + pub(crate) fn take_call_value(&mut self) -> JsValue { self.pending_effect .call_value .take() diff --git a/src/engine/vm/bindings.rs b/src/engine/vm/bindings.rs index a6d8cc6f..3a3d6b6a 100644 --- a/src/engine/vm/bindings.rs +++ b/src/engine/vm/bindings.rs @@ -11,11 +11,11 @@ use crate::engine::code::function::metadata::{ClosureVariable, ClosureVariableKi use crate::engine::heap::RawValue; use crate::engine::heap::roots::VarRefRoot; use crate::engine::object::{CallableRef, PrivateNameRef}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::exception::runtime_error_to_vm_error; pub(in crate::engine::vm) enum FrameBinding { - Direct(Value), + Direct(JsValue), Private(PrivateNameRef), PrivateCallable(CallableRef), Uninitialized, @@ -32,6 +32,24 @@ pub(in crate::engine::vm) const fn is_private_callable_kind(kind: ClosureVariabl ) } +/// Release every owner a frame binding carried. Direct internal values take +/// the deferred-release path; the rooted private/captured wrappers release +/// through their own `Drop`. +pub(in crate::engine::vm) fn release_frame_binding( + runtime: &Runtime, + binding: FrameBinding, +) -> Result<(), Error> { + match binding { + FrameBinding::Direct(value) => runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error), + FrameBinding::Private(_) + | FrameBinding::PrivateCallable(_) + | FrameBinding::Uninitialized + | FrameBinding::Captured(_) => Ok(()), + } +} + /// Read a freshly authenticated shared cell without creating any owner or /// operation boundary. Pending releases must take the canonical path because /// its RuntimeOperation drains them before observing the cell. @@ -39,7 +57,7 @@ pub(in crate::engine::vm) const fn is_private_callable_kind(kind: ClosureVariabl pub(in crate::engine::vm) fn read_immediate_cell( runtime: &Runtime, root: &impl crate::engine::heap::roots::VarRefHandle, -) -> Option { +) -> Option { if !root.belongs_to(runtime) || runtime.0.deferred_references.has_pending() { return None; } @@ -49,29 +67,37 @@ pub(in crate::engine::vm) fn read_immediate_cell( return None; } match &cell.value { - RawValue::Undefined => Some(Value::Undefined), - RawValue::Null => Some(Value::Null), - RawValue::Bool(value) => Some(Value::Bool(*value)), - RawValue::Int(value) => Some(Value::Int(*value)), - RawValue::Float(value) => Some(Value::Float(*value)), + RawValue::Undefined => Some(JsValue::Undefined), + RawValue::Null => Some(JsValue::Null), + RawValue::Bool(value) => Some(JsValue::Bool(*value)), + RawValue::Int(value) => Some(JsValue::Int(*value)), + RawValue::Float(value) => Some(JsValue::Float(*value)), _ => None, } } /// Keep the scalar read cheap; only a non-immediate miss attempts an owned -/// read under the shared heap guard. The flag distinguishes profiling events. +/// shared-borrow read. `None` declines to the ordinary binding path. The flag +/// distinguishes profiling events; a trusted non-immediate read never fails, +/// so a stale or sentinel cell panics instead of returning an error. #[inline] pub(in crate::engine::vm) fn read_run_cell( runtime: &Runtime, root: &impl crate::engine::heap::roots::VarRefHandle, -) -> Result, Error> { +) -> Option<(JsValue, bool)> { if let Some(value) = read_immediate_cell(runtime, root) { - return Ok(Some((value, false))); + return Some((value, false)); + } + if let Some(value) = runtime.read_owned_cell_fast(root) { + return Some((value, true)); } + // Cold decline: Symbols need an atom-table retain, and other cases fall + // back to the ordinary binding path when this returns `None`. runtime .try_read_owned_var_ref(root) - .map(|value| value.map(|value| (value, true))) - .map_err(runtime_error_to_vm_error) + .ok() + .flatten() + .map(|value| (value, true)) } /// Commit only a no-owner immediate replacement. The caller first proves its @@ -81,15 +107,15 @@ pub(in crate::engine::vm) fn read_run_cell( pub(in crate::engine::vm) fn try_write_immediate_cell( runtime: &Runtime, root: &impl crate::engine::heap::roots::VarRefHandle, - value: &Value, + value: &JsValue, expected: Option<(bool, bool, ClosureVariableKind)>, ) -> bool { let replacement = match value { - Value::Undefined => RawValue::Undefined, - Value::Null => RawValue::Null, - Value::Bool(value) => RawValue::Bool(*value), - Value::Int(value) => RawValue::Int(*value), - Value::Float(value) => RawValue::Float(*value), + JsValue::Undefined => RawValue::Undefined, + JsValue::Null => RawValue::Null, + JsValue::Bool(value) => RawValue::Bool(*value), + JsValue::Int(value) => RawValue::Int(*value), + JsValue::Float(value) => RawValue::Float(*value), _ => return false, }; if !root.belongs_to(runtime) || runtime.0.deferred_references.has_pending() { @@ -131,9 +157,11 @@ pub(crate) fn closure_view_matches_cell( pub(in crate::engine::vm) fn read_frame_binding( runtime: &Runtime, binding: &FrameBinding, -) -> Result { +) -> Result { match binding { - FrameBinding::Direct(value) => Ok(value.clone()), + FrameBinding::Direct(value) => runtime + .dup_jsvalue(value) + .map_err(|error| Error::internal(error.to_string())), FrameBinding::Private(_) | FrameBinding::PrivateCallable(_) => Err(Error::internal( "ordinary local read reached a private-element binding", )), @@ -152,15 +180,21 @@ pub(in crate::engine::vm) fn capture_frame_binding( descriptor: ClosureVariable, ) -> Result { match binding { - FrameBinding::Direct(value) => { + FrameBinding::Direct(_) => { if descriptor.kind.is_private() { return Err(Error::internal( "private-name capture reached an ordinary frame value", )); } + // Move the direct owner into the shared cell; `new_var_ref` + // consumes its edges and the capture replaces the binding. + let owned = std::mem::replace(binding, FrameBinding::Uninitialized); + let FrameBinding::Direct(value) = owned else { + unreachable!("direct binding authenticated before the move") + }; let root = runtime .new_var_ref( - value.clone(), + value, descriptor.is_lexical, descriptor.is_const, descriptor.kind, @@ -255,11 +289,16 @@ pub(in crate::engine::vm) fn close_frame_binding( "captured private-element cell contains an incompatible value", )); } - raw => FrameBinding::Direct( - runtime - .root_raw_value(&raw) - .map_err(runtime_error_to_vm_error)?, - ), + raw => { + let value = JsValue::from_raw(raw).ok_or_else(|| { + Error::internal("captured cell contained an internal value sentinel") + })?; + FrameBinding::Direct( + runtime + .dup_jsvalue(&value) + .map_err(runtime_error_to_vm_error)?, + ) + } }; *binding = detached; Ok(()) @@ -272,7 +311,7 @@ pub(in crate::engine::vm) fn finish_derived_return( caller_realm: crate::engine::heap::ContextId, definition: crate::engine::code::function::metadata::VariableDefinition, binding: Option<&FrameBinding>, - value: Value, + value: JsValue, ) -> Result { use crate::engine::api::error::NativeErrorKind; use crate::engine::vm::Completion; @@ -285,11 +324,13 @@ pub(in crate::engine::vm) fn finish_derived_return( )); } match value { - value @ Value::Object(_) => Ok(Completion::Return(value)), - Value::Undefined => { + value @ JsValue::Object(_) => Ok(Completion::Return(value)), + JsValue::Undefined => { let binding = binding.ok_or_else(|| Error::internal("local index is out of bounds"))?; let this_value = match binding { - FrameBinding::Direct(value) => value.clone(), + FrameBinding::Direct(value) => runtime + .dup_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, FrameBinding::Private(_) | FrameBinding::PrivateCallable(_) => { return Err(Error::internal( "derived this local contains a private-element identity", @@ -297,7 +338,7 @@ pub(in crate::engine::vm) fn finish_derived_return( } FrameBinding::Uninitialized => { return runtime - .new_native_error( + .new_native_error_jsvalue( caller_realm, NativeErrorKind::Reference, "this is not initialized", @@ -311,7 +352,7 @@ pub(in crate::engine::vm) fn finish_derived_return( .map_err(runtime_error_to_vm_error)?; if matches!(raw, RawValue::Uninitialized) { return runtime - .new_native_error( + .new_native_error_jsvalue( caller_realm, NativeErrorKind::Reference, "this is not initialized", @@ -319,26 +360,34 @@ pub(in crate::engine::vm) fn finish_derived_return( .map(Completion::Throw) .map_err(runtime_error_to_vm_error); } + let value = JsValue::from_raw(raw).ok_or_else(|| { + Error::internal("captured this cell held an internal value sentinel") + })?; runtime - .root_raw_value(&raw) + .dup_jsvalue(&value) .map_err(runtime_error_to_vm_error)? } }; - if !matches!(this_value, Value::Object(_)) { + if !matches!(this_value, JsValue::Object(_)) { return Err(Error::internal( "initialized derived this binding did not contain an Object", )); } Ok(Completion::Return(this_value)) } - _ => runtime - .new_native_error( - caller_realm, - NativeErrorKind::Type, - "derived class constructor must return an object or undefined", - ) - .map(Completion::Throw) - .map_err(runtime_error_to_vm_error), + _ => { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + runtime + .new_native_error_jsvalue( + caller_realm, + NativeErrorKind::Type, + "derived class constructor must return an object or undefined", + ) + .map(Completion::Throw) + .map_err(runtime_error_to_vm_error) + } } } @@ -348,7 +397,7 @@ pub(in crate::engine::vm) fn initialize_derived_binding( runtime: &Runtime, definition: crate::engine::code::function::metadata::VariableDefinition, binding: Option<&FrameBinding>, - value: Value, + value: JsValue, ) -> Result, Error> { use crate::engine::api::error::ErrorKind; if !definition.is_lexical @@ -359,7 +408,7 @@ pub(in crate::engine::vm) fn initialize_derived_binding( "derived this initialization referenced a non-mutable lexical local", )); } - if !matches!(value, Value::Object(_)) { + if !matches!(value, JsValue::Object(_)) { return Err(Error::internal( "derived this initialization did not receive an Object", )); @@ -484,7 +533,7 @@ pub(in crate::engine::vm) fn read_checked_closure( root: &impl crate::engine::heap::roots::VarRefHandle, descriptor: ClosureVariable, strip_variable_debug: bool, -) -> Result { +) -> Result { let raw = runtime .raw_var_ref_value(root) .map_err(runtime_error_to_vm_error)?; @@ -496,8 +545,10 @@ pub(in crate::engine::vm) fn read_checked_closure( strip_variable_debug, )?); } + let value = JsValue::from_raw(raw) + .ok_or_else(|| Error::internal("captured cell held an internal value sentinel"))?; runtime - .root_raw_value(&raw) + .dup_jsvalue(&value) .map_err(runtime_error_to_vm_error) } @@ -506,7 +557,7 @@ pub(in crate::engine::vm) fn write_checked_closure( root: &impl crate::engine::heap::roots::VarRefHandle, descriptor: ClosureVariable, strip_variable_debug: bool, - value: Value, + value: JsValue, ) -> Result<(), Error> { let (uninitialized, is_const) = { let state = runtime.0.state.borrow(); @@ -595,23 +646,20 @@ pub(in crate::engine::vm) fn initialize_local_binding( runtime: &Runtime, kind: ClosureVariableKind, binding: &mut FrameBinding, - value: Value, + value: JsValue, ) -> Result<(), Error> { - if kind == ClosureVariableKind::WithObject { - let Value::Object(object) = &value else { - return Err(Error::internal( - "with-object initialization did not receive an Object", - )); - }; - if !object.belongs_to(runtime) { - return Err(Error::internal( - "with-object initialization received a cross-runtime Object", - )); - } + if kind == ClosureVariableKind::WithObject && !matches!(value, JsValue::Object(_)) { + return Err(Error::internal( + "with-object initialization did not receive an Object", + )); } match binding { FrameBinding::Direct(slot) => { - *slot = value; + // Overwrite releases the replaced owner and moves the new one in. + let previous = std::mem::replace(slot, value); + runtime + .release_jsvalue(previous) + .map_err(runtime_error_to_vm_error)?; Ok(()) } FrameBinding::Private(_) | FrameBinding::PrivateCallable(_) => Err(Error::internal( @@ -631,7 +679,7 @@ pub(in crate::engine::vm) fn initialize_derived_closure( runtime: &Runtime, root: &impl crate::engine::heap::roots::VarRefHandle, descriptor: ClosureVariable, - value: Value, + value: JsValue, ) -> Result<(), Error> { use crate::engine::api::error::ErrorKind; if !descriptor.is_lexical @@ -642,7 +690,7 @@ pub(in crate::engine::vm) fn initialize_derived_closure( "derived this initialization referenced a non-mutable lexical closure", )); } - if !matches!(value, Value::Object(_)) { + if !matches!(value, JsValue::Object(_)) { return Err(Error::internal( "derived this initialization did not receive an Object", )); @@ -683,6 +731,7 @@ pub(super) fn validate_module_import_collision(descriptor: ClosureVariable) -> R #[cfg(test)] mod immediate_cell_tests { use super::*; + use crate::engine::value::Value; #[test] #[cfg(feature = "profiling")] @@ -722,26 +771,26 @@ mod immediate_cell_tests { fn immediate_cell_writes_commit_only_mutable_initialized_owners() { let runtime = Runtime::new(); let root = runtime - .new_var_ref(Value::Int(1), true, false, ClosureVariableKind::Normal) + .new_var_ref_rooted(Value::Int(1), true, false, ClosureVariableKind::Normal) .unwrap(); let metadata = Some((true, false, ClosureVariableKind::Normal)); assert!(try_write_immediate_cell( &runtime, &root, - &Value::Int(2), + &JsValue::Int(2), metadata )); - assert_eq!(runtime.read_var_ref(&root).unwrap(), Value::Int(2)); + assert_eq!(runtime.read_var_ref_rooted(&root).unwrap(), Value::Int(2)); assert!(!try_write_immediate_cell( &runtime, &root, - &Value::Int(3), + &JsValue::Int(3), Some((false, false, ClosureVariableKind::Normal)) )); assert!(!try_write_immediate_cell( &Runtime::new(), &root, - &Value::Int(3), + &JsValue::Int(3), metadata )); { @@ -749,24 +798,25 @@ mod immediate_cell_tests { assert!(!try_write_immediate_cell( &runtime, &root, - &Value::Int(3), + &JsValue::Int(3), metadata )); } + let object = runtime + .into_jsvalue(Value::Object(runtime.new_object(None).unwrap())) + .unwrap(); assert!(!try_write_immediate_cell( - &runtime, - &root, - &Value::Object(runtime.new_object(None).unwrap()), - metadata + &runtime, &root, &object, metadata )); - assert_eq!(runtime.read_var_ref(&root).unwrap(), Value::Int(2)); + runtime.release_jsvalue(object).unwrap(); + assert_eq!(runtime.read_var_ref_rooted(&root).unwrap(), Value::Int(2)); assert!(try_write_immediate_cell( &runtime, &root, - &Value::Float(-0.0), + &JsValue::Float(-0.0), metadata )); - let Value::Float(value) = runtime.read_var_ref(&root).unwrap() else { + let Value::Float(value) = runtime.read_var_ref_rooted(&root).unwrap() else { panic!("expected float"); }; assert!(value.is_sign_negative()); @@ -774,35 +824,38 @@ mod immediate_cell_tests { assert!(!try_write_immediate_cell( &runtime, &root, - &Value::Int(3), + &JsValue::Int(3), metadata )); runtime - .write_var_ref(&root, Value::Object(runtime.new_object(None).unwrap())) + .write_var_ref_rooted(&root, Value::Object(runtime.new_object(None).unwrap())) .unwrap(); assert!(!try_write_immediate_cell( &runtime, &root, - &Value::Int(3), + &JsValue::Int(3), metadata )); let constant = runtime - .new_var_ref(Value::Int(1), true, true, ClosureVariableKind::Normal) + .new_var_ref_rooted(Value::Int(1), true, true, ClosureVariableKind::Normal) .unwrap(); assert!(!try_write_immediate_cell( &runtime, &constant, - &Value::Int(3), + &JsValue::Int(3), None )); - assert_eq!(runtime.read_var_ref(&constant).unwrap(), Value::Int(1)); + assert_eq!( + runtime.read_var_ref_rooted(&constant).unwrap(), + Value::Int(1) + ); } #[test] fn immediate_cell_writes_preserve_deferred_release_boundary() { let runtime = Runtime::new(); let root = runtime - .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .new_var_ref_rooted(Value::Int(1), false, false, ClosureVariableKind::Normal) .unwrap(); let object = runtime.new_object(None).unwrap(); { @@ -812,11 +865,11 @@ mod immediate_cell_tests { assert!(!try_write_immediate_cell( &runtime, &root, - &Value::Int(2), + &JsValue::Int(2), None )); assert!(runtime.0.deferred_references.has_pending()); - assert_eq!( + assert!(matches!( runtime .0 .state @@ -826,12 +879,12 @@ mod immediate_cell_tests { .unwrap() .value, RawValue::Int(1) - ); + )); runtime.drain_deferred_references().unwrap(); assert!(try_write_immediate_cell( &runtime, &root, - &Value::Int(2), + &JsValue::Int(2), None )); } @@ -905,18 +958,18 @@ mod immediate_cell_tests { fn immediate_cell_reads_are_fresh_and_preserve_fallback_boundaries() { let runtime = Runtime::new(); let root = runtime - .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .new_var_ref_rooted(Value::Int(1), false, false, ClosureVariableKind::Normal) .unwrap(); - assert_eq!(read_immediate_cell(&runtime, &root), Some(Value::Int(1))); - for value in [ - Value::Null, - Value::Undefined, - Value::Bool(true), - Value::Float(-0.0), - Value::Int(7), + assert_eq!(read_immediate_cell(&runtime, &root), Some(JsValue::Int(1))); + for (value, expected) in [ + (Value::Null, JsValue::Null), + (Value::Undefined, JsValue::Undefined), + (Value::Bool(true), JsValue::Bool(true)), + (Value::Float(-0.0), JsValue::Float(-0.0)), + (Value::Int(7), JsValue::Int(7)), ] { - runtime.write_var_ref(&root, value.clone()).unwrap(); - assert_eq!(read_immediate_cell(&runtime, &root), Some(value)); + runtime.write_var_ref_rooted(&root, value).unwrap(); + assert_eq!(read_immediate_cell(&runtime, &root), Some(expected)); } let foreign = Runtime::new(); assert!(read_immediate_cell(&foreign, &root).is_none()); @@ -925,17 +978,17 @@ mod immediate_cell_tests { assert!(read_immediate_cell(&runtime, &root).is_none()); } runtime - .write_var_ref(&root, Value::Object(runtime.new_object(None).unwrap())) + .write_var_ref_rooted(&root, Value::Object(runtime.new_object(None).unwrap())) .unwrap(); assert!(read_immediate_cell(&runtime, &root).is_none()); runtime.reset_var_ref_uninitialized(&root).unwrap(); assert!(read_immediate_cell(&runtime, &root).is_none()); let constant = runtime - .new_var_ref(Value::Int(9), true, true, ClosureVariableKind::Normal) + .new_var_ref_rooted(Value::Int(9), true, true, ClosureVariableKind::Normal) .unwrap(); assert_eq!( read_immediate_cell(&runtime, &constant), - Some(Value::Int(9)) + Some(JsValue::Int(9)) ); } @@ -943,7 +996,7 @@ mod immediate_cell_tests { fn immediate_cell_reads_never_drain_deferred_owners() { let runtime = Runtime::new(); let root = runtime - .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .new_var_ref_rooted(Value::Int(1), false, false, ClosureVariableKind::Normal) .unwrap(); let object = runtime.new_object(None).unwrap(); { @@ -954,7 +1007,7 @@ mod immediate_cell_tests { assert!(read_immediate_cell(&runtime, &root).is_none()); assert!(runtime.0.deferred_references.has_pending()); runtime.drain_deferred_references().unwrap(); - assert_eq!(read_immediate_cell(&runtime, &root), Some(Value::Int(1))); + assert_eq!(read_immediate_cell(&runtime, &root), Some(JsValue::Int(1))); } #[test] diff --git a/src/engine/vm/call.rs b/src/engine/vm/call.rs index 8f4accf0..a25a247e 100644 --- a/src/engine/vm/call.rs +++ b/src/engine/vm/call.rs @@ -25,8 +25,8 @@ use crate::engine::code::rooted::FunctionBytecodeRef; use crate::engine::heap::{ContextId, ObjectPayload}; use crate::engine::object::{CallableRef, ObjectRef}; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; impl Runtime { @@ -74,10 +74,17 @@ impl Runtime { drop(state); let target = ObjectRef::from_borrowed_handle(self.clone(), target)?; let target = CallableRef::from_validated_object(target); - let this_value = self.root_raw_value(&this_value)?; + let to_internal = + |raw: &crate::engine::heap::RawValue| -> Result { + let value = JsValue::from_raw(raw.clone()).ok_or( + RuntimeError::Invariant("bound value was an internal sentinel"), + )?; + self.dup_jsvalue(&value) + }; + let this_value = to_internal(&this_value)?; let arguments = arguments .iter() - .map(|argument| self.root_raw_value(argument)) + .map(to_internal) .collect::, _>>()?; #[cfg(feature = "profiling")] { @@ -86,7 +93,7 @@ impl Runtime { arguments.capacity(), size_of::(), ); - crate::engine::api::profiling::record_call_buffer_copies( + crate::engine::api::profiling::record_call_buffer_js_value_copies( "bound.rooted_snapshot", &arguments, ); @@ -250,7 +257,7 @@ impl Runtime { target, min_readable_args, NativeInvocation::Call { - this_value: iterator, + this_value: self.unroot_value(&iterator)?, }, &[], NativeInvokeMode::IteratorNextRaw, @@ -304,7 +311,9 @@ impl Runtime { ) -> Result { let constructor = match self.constructor_from_value(caller_realm, function)? { NativeConversion::Value(constructor) => constructor, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.unroot_value(&value)?)); + } }; self.construct_constructor_with_raw_new_target_internal( caller_realm, @@ -325,7 +334,7 @@ impl Runtime { self.construct_internal_with_new_target( caller_realm, constructor, - ConstructNewTarget::Raw(new_target), + ConstructNewTarget::Raw(self.unroot_value(&new_target)?), arguments, ) } @@ -371,7 +380,9 @@ impl Runtime { let (constructor, new_target) = match self.prepare_constructor_pair(caller_realm, constructor, new_target)? { NativeConversion::Value(pair) => pair, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.unroot_value(&value)?)); + } }; self.construct_constructor_internal(caller_realm, &constructor, &new_target, arguments) } @@ -425,7 +436,7 @@ impl Runtime { caller_realm: ContextId, mut constructor: ConstructorRef, mut new_target: ConstructNewTarget, - mut arguments: Vec, + mut arguments: Vec, ) -> Result, RuntimeError> { self.0.state.borrow().heap.context(caller_realm)?; if !constructor.as_object().belongs_to(self) { @@ -435,14 +446,9 @@ impl Runtime { ConstructNewTarget::Validated(target) if !target.as_object().belongs_to(self) => { return Err(RuntimeError::WrongRuntime("constructor")); } - ConstructNewTarget::Raw(value) => { - self.validate_value_domain(value, "raw construct new target")? - } + // Internal values carry no runtime branding. _ => {} } - for argument in &arguments { - self.validate_value_domain(argument, "construct argument")?; - } loop { if !self.is_constructor(constructor.as_object())? { return Ok(NativeConversion::Throw(self.new_not_constructor_error( @@ -466,13 +472,19 @@ impl Runtime { arguments: bound, .. } => { - arguments = - match self.concatenate_bound_arguments(caller_realm, &bound, &arguments)? { - NativeConversion::Value(arguments) => arguments, - NativeConversion::Throw(value) => { - return Ok(NativeConversion::Throw(value)); - } - }; + // The bound payload roots transfer into internal values + // without a retain/release pair; the accumulated argument + // edges move into the merged buffer. + arguments = match self.concatenate_bound_arguments_jsvalue( + caller_realm, + bound, + arguments, + )? { + NativeConversion::Value(arguments) => arguments, + NativeConversion::Throw(value) => { + return Ok(NativeConversion::Throw(value)); + } + }; new_target.retarget_bound_identity(&constructor, &target); constructor = ConstructorRef::from_validated_callable(&target); } @@ -497,6 +509,12 @@ impl Runtime { new_target: ConstructNewTarget, arguments: &[Value], ) -> Result { + // Public-root arguments entering the internal constructor convention + // are duplicated; the caller's roots release through their Drop path. + let arguments = arguments + .iter() + .map(|argument| self.unroot_value(argument)) + .collect::, _>>()?; let NormalizedConstructor { target, new_target, @@ -505,11 +523,20 @@ impl Runtime { caller_realm, constructor.clone(), new_target, - arguments.to_vec(), + arguments, )? { NativeConversion::Value(result) => result, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } }; + // This synchronous host path consumes public roots; the normalized + // internal owners are rooted at this boundary and their internal + // edges are released. + let arguments = arguments + .into_iter() + .map(|argument| self.root_and_release_jsvalue(argument)) + .collect::, _>>()?; let (callable, classification) = match target { ConstructorTarget::Proxy(constructor) => { return self.construct_proxy(caller_realm, &constructor, new_target, &arguments); @@ -535,7 +562,7 @@ impl Runtime { execution_realm, target, min_readable_args, - new_target.value(), + self.root_and_release_jsvalue(new_target.into_value())?, &arguments, ) } @@ -562,15 +589,15 @@ impl Runtime { caller_realm, &callable, Value::Undefined, - new_target.value(), + self.root_and_release_jsvalue(new_target.into_value())?, &arguments, bytecode, closure_slots, )?; return match completion { - Completion::Return(value @ Value::Object(_)) => { - Ok(Completion::Return(value)) - } + Completion::Return( + value @ crate::engine::value::JsValue::Object(_), + ) => Ok(Completion::Return(value)), Completion::Throw(value) => Ok(Completion::Throw(value)), Completion::Return(_) => Err(RuntimeError::Invariant( "derived constructor bytecode returned an unvalidated primitive", @@ -579,23 +606,26 @@ impl Runtime { } ConstructorKind::Base => {} } - let raw_new_target = new_target.value(); + let raw_new_target = self.root_and_release_jsvalue(new_target.into_value())?; let this_value = match self.create_from_constructor_value(caller_realm, &raw_new_target)? { Completion::Return(value) => value, Completion::Throw(value) => return Ok(Completion::Throw(value)), }; + let this_argument = self.root_value(&this_value)?; let completion = self.execute_bytecode_callable( caller_realm, &callable, - this_value.clone(), + this_argument, raw_new_target, &arguments, bytecode, closure_slots, )?; Ok(match completion { - Completion::Return(value @ Value::Object(_)) => Completion::Return(value), + Completion::Return(value @ crate::engine::value::JsValue::Object(_)) => { + Completion::Return(value) + } Completion::Throw(value) => Completion::Throw(value), Completion::Return(_) => Completion::Return(this_value), }) @@ -624,7 +654,7 @@ impl Runtime { new_target: &Value, ) -> Result { let reply = if matches!(new_target, Value::Undefined) { - Completion::Return(Value::Undefined) + Completion::Return(JsValue::Undefined) } else { let key = self.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?; @@ -641,22 +671,26 @@ impl Runtime { ) -> Result { let prototype = match reply { result @ Completion::Throw(_) => return Ok(result), - Completion::Return(Value::Object(prototype)) => prototype, + Completion::Return(JsValue::Object(prototype)) => { + ObjectRef::from_owned_handle(self.clone(), prototype) + } Completion::Return(_) => { let realm = if matches!(new_target, Value::Undefined) { caller_realm } else { match self.function_realm_from_value(caller_realm, new_target)? { NativeConversion::Value(realm) => realm, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(self.into_jsvalue(value)?)); + } } }; let prototype = self.0.state.borrow().heap.context(realm)?.object_prototype; ObjectRef::from_borrowed_handle(self.clone(), prototype)? } }; - Ok(Completion::Return(Value::Object( - self.new_object(Some(&prototype))?, + Ok(Completion::Return(JsValue::Object( + self.new_object(Some(&prototype))?.into_handle(), ))) } @@ -675,7 +709,9 @@ impl Runtime { realm, target, min_readable_args, - NativeInvocation::Construct { new_target }, + NativeInvocation::Construct { + new_target: self.unroot_value(&new_target)?, + }, arguments, NativeInvokeMode::Ordinary, )?; @@ -697,7 +733,9 @@ impl Runtime { realm, target, min_readable_args, - NativeInvocation::Call { this_value }, + NativeInvocation::Call { + this_value: self.unroot_value(&this_value)?, + }, arguments, NativeInvokeMode::Ordinary, )?; @@ -781,12 +819,59 @@ impl Runtime { } } -#[derive(Clone)] pub(crate) enum NativeInvocation { - Call { this_value: Value }, - Construct { new_target: Value }, - Getter { this_value: Value }, - Setter { this_value: Value }, + Call { + this_value: crate::engine::value::JsValue, + }, + Construct { + new_target: crate::engine::value::JsValue, + }, + Getter { + this_value: crate::engine::value::JsValue, + }, + Setter { + this_value: crate::engine::value::JsValue, + }, +} + +impl NativeInvocation { + /// Release the internal edge this invocation owns. Boundary adapters that + /// duplicate an invocation through [`NativeInvocation::dup`] must release + /// their own copy once the borrowed step has captured its own edges. + pub(crate) fn release( + self, + runtime: &crate::engine::api::runtime::Runtime, + ) -> Result<(), crate::engine::api::runtime_error::RuntimeError> { + let value = match self { + Self::Call { this_value } + | Self::Getter { this_value } + | Self::Setter { this_value } => this_value, + Self::Construct { new_target } => new_target, + }; + runtime.release_jsvalue(value) + } + + /// Duplicate the invocation's internal value edges. There is no automatic + /// `Clone` because duplicating a handle needs the owning runtime. + pub(crate) fn dup( + &self, + runtime: &crate::engine::api::runtime::Runtime, + ) -> Result { + Ok(match self { + Self::Call { this_value } => Self::Call { + this_value: runtime.dup_jsvalue(this_value)?, + }, + Self::Construct { new_target } => Self::Construct { + new_target: runtime.dup_jsvalue(new_target)?, + }, + Self::Getter { this_value } => Self::Getter { + this_value: runtime.dup_jsvalue(this_value)?, + }, + Self::Setter { this_value } => Self::Setter { + this_value: runtime.dup_jsvalue(this_value)?, + }, + }) + } } pub(crate) enum NativeInvocationAdaptation { @@ -804,7 +889,10 @@ pub(crate) enum NativeInvocationAdaptation { /// return an already-materialized result object (`pdone == 2`). pub(crate) enum NativeInvokeOutcome { Completion(Completion), - IteratorNextRaw { value: Value, done: bool }, + IteratorNextRaw { + value: crate::engine::value::JsValue, + done: bool, + }, } #[derive(Clone, Copy)] @@ -815,7 +903,7 @@ pub(crate) enum NativeInvokeMode { pub(crate) struct NativeArguments { pub(crate) actual_arg_count: usize, - pub(crate) readable: Vec, + pub(crate) readable: Vec, } /// Result of QuickJS `Get(newTarget, "prototype")` followed by @@ -841,8 +929,8 @@ pub(crate) enum CallableExecution { }, Bound { target: CallableRef, - this_value: Value, - arguments: Vec, + this_value: crate::engine::value::JsValue, + arguments: Vec, }, Proxy, } @@ -867,6 +955,12 @@ impl ConstructorRef { pub(crate) fn as_object(&self) -> &ObjectRef { &self.0 } + + /// Consume this validated constructor root, transferring its one owned + /// object edge to the caller without retaining or releasing. + pub(crate) fn into_object(self) -> ObjectRef { + self.0 + } } /// Whether one internal constructor entry carries an ECMAScript-validated @@ -875,24 +969,64 @@ impl ConstructorRef { /// `OP_call_constructor`, `OP_apply` constructor mode, and derived `super()` /// use the raw form. Public Context and Reflect entry points retain the /// validated form and its existing constructor checks. -#[derive(Clone)] pub(crate) enum ConstructNewTarget { Validated(ConstructorRef), - Raw(Value), + Raw(crate::engine::value::JsValue), } impl ConstructNewTarget { - pub(crate) fn value(&self) -> Value { + /// Consume into the internal new-target value, transferring the validated + /// constructor's edge or moving the raw owner. + pub(crate) fn into_value(self) -> crate::engine::value::JsValue { match self { - Self::Validated(constructor) => Value::Object(constructor.as_object().clone()), - Self::Raw(value) => value.clone(), + Self::Validated(constructor) => { + crate::engine::value::JsValue::Object(constructor.into_object().into_handle()) + } + Self::Raw(value) => value, + } + } + + /// Borrow as the internal new-target value without transferring the edge. + /// Callers must not release through the returned value. + pub(crate) fn value(&self) -> crate::engine::value::JsValue { + match self { + Self::Validated(constructor) => { + crate::engine::value::JsValue::Object(constructor.as_object().object_id()) + } + Self::Raw(value) => match value { + crate::engine::value::JsValue::Undefined => { + crate::engine::value::JsValue::Undefined + } + crate::engine::value::JsValue::Null => crate::engine::value::JsValue::Null, + crate::engine::value::JsValue::Bool(value) => { + crate::engine::value::JsValue::Bool(*value) + } + crate::engine::value::JsValue::Int(value) => { + crate::engine::value::JsValue::Int(*value) + } + crate::engine::value::JsValue::Float(value) => { + crate::engine::value::JsValue::Float(*value) + } + crate::engine::value::JsValue::String(id) => { + crate::engine::value::JsValue::String(*id) + } + crate::engine::value::JsValue::BigInt(id) => { + crate::engine::value::JsValue::BigInt(*id) + } + crate::engine::value::JsValue::Symbol(index) => { + crate::engine::value::JsValue::Symbol(*index) + } + crate::engine::value::JsValue::Object(id) => { + crate::engine::value::JsValue::Object(*id) + } + }, } } pub(crate) fn retarget_bound_identity(&mut self, bound: &ConstructorRef, target: &CallableRef) { let matches_bound = match self { Self::Validated(constructor) => constructor.as_object() == bound.as_object(), - Self::Raw(Value::Object(object)) => object == bound.as_object(), + Self::Raw(JsValue::Object(object)) => *object == bound.as_object().object_id(), Self::Raw(_) => false, }; if !matches_bound { @@ -902,7 +1036,9 @@ impl ConstructNewTarget { Self::Validated(constructor) => { *constructor = ConstructorRef::from_validated_callable(target); } - Self::Raw(value) => *value = Value::Object(target.as_object().clone()), + Self::Raw(value) => { + *value = JsValue::Object(target.as_object().clone().into_handle()); + } } } } @@ -921,7 +1057,7 @@ pub(crate) enum DirectCallTarget { pub(crate) struct NormalizedConstructor { pub target: ConstructorTarget, pub new_target: ConstructNewTarget, - pub arguments: Vec, + pub arguments: Vec, } pub(crate) enum ConstructorTarget { Proxy(ConstructorRef), diff --git a/src/engine/vm/call/native.rs b/src/engine/vm/call/native.rs index 9e406352..cece1308 100644 --- a/src/engine/vm/call/native.rs +++ b/src/engine/vm/call/native.rs @@ -7,7 +7,7 @@ use crate::engine::{ builtins::native::NativeFunctionId, heap::ContextId, object::CallableRef, - value::Value, + value::{JsValue, Value}, vm::{Completion, frames::ActiveFrameGuard}, }; @@ -17,8 +17,9 @@ pub(in crate::engine::vm) struct PreparedNativeCall { } pub(in crate::engine::vm) struct NativeActivation { + runtime: Runtime, // Retire the non-owning diagnostic descriptor before callable roots on unwind. - active_frame: ActiveFrameGuard, + active_frame: Option, pub callable: CallableRef, pub realm: ContextId, pub target: NativeFunctionId, @@ -142,7 +143,7 @@ impl Runtime { let target = NativeFunctionId::ArrayIteratorNext; let mode = NativeInvokeMode::IteratorNextRaw; let invocation = NativeInvocation::Call { - this_value: receiver, + this_value: self.unroot_value(&receiver)?, }; if min_readable_args != 0 { return self.prepare_native_continuation_owned( @@ -163,6 +164,7 @@ impl Runtime { crate::engine::api::profiling::record_owned_execution_event("native_activation_prepared"); Ok(PreparedNativeCall { activation: NativeActivation { + runtime: self.clone(), callable, realm, target, @@ -171,7 +173,7 @@ impl Runtime { actual_arg_count: 0, readable: Vec::new(), }, - active_frame, + active_frame: Some(active_frame), }, invocation, }) @@ -247,34 +249,41 @@ impl Runtime { let available_arg_count = actual_arg_count.max(usize::from(min_readable_args)); let (mut readable, _copied, _before) = match arguments { NativeArgumentInput::Borrowed(values) => { - let mut readable = Vec::new(); + let mut readable: Vec = Vec::new(); readable.try_reserve(available_arg_count).map_err(|_| { RuntimeError::Invariant("native readable arguments allocation failed") })?; - readable.extend_from_slice(values); + for value in values { + readable.push(self.unroot_value(value)?); + } (readable, true, 0) } - NativeArgumentInput::Owned(mut values) => { + NativeArgumentInput::Owned(values) => { let before = values.capacity(); + let mut readable: Vec = + Vec::with_capacity(values.len()); + for value in values { + readable.push(self.into_jsvalue(value)?); + } // All padding allocation precedes publication. Actual arity // and every extra argument survive this owning handoff. - values + readable .try_reserve(available_arg_count - actual_arg_count) .map_err(|_| { RuntimeError::Invariant("native readable arguments allocation failed") })?; - (values, false, before) + (readable, false, before) } }; - if actual_arg_count < available_arg_count { - readable.resize(available_arg_count, Value::Undefined); + while readable.len() < available_arg_count { + readable.push(crate::engine::value::JsValue::Undefined); } #[cfg(feature = "profiling")] { use crate::engine::api::profiling::{ - record_call_buffer_capacity, record_call_buffer_copies, - record_call_buffer_initialized, record_call_buffer_observed, + record_call_buffer_capacity, record_call_buffer_initialized, + record_call_buffer_js_value_copies, record_call_buffer_observed, }; record_call_buffer_capacity( "native.readable", @@ -283,7 +292,10 @@ impl Runtime { size_of::(), ); if _copied { - record_call_buffer_copies("native.readable", &readable[..actual_arg_count]); + record_call_buffer_js_value_copies( + "native.readable", + &readable[..actual_arg_count], + ); } else { record_call_buffer_observed("native.incoming_argv", _before, size_of::()); // Moving Vec ownership into NativeArguments does not move elements. @@ -309,22 +321,39 @@ impl Runtime { crate::engine::api::profiling::record_owned_execution_event("native_activation_prepared"); Ok(PreparedNativeCall { activation: NativeActivation { + runtime: self.clone(), callable: callable_input.into_owned(), realm, target, mode, arguments, - active_frame, + active_frame: Some(active_frame), }, invocation, }) } } +impl Drop for NativeActivation { + /// Release every readable argument edge still owned when the activation is + /// abandoned without `finish`. `finish` takes the buffer first, so a + /// completed activation drops an empty vector. Releases are defer-safe and + /// never run JavaScript. + fn drop(&mut self) { + let runtime = self.runtime.clone(); + for value in self.arguments.readable.drain(..) { + let _ = runtime.release_jsvalue(value); + } + } +} + impl NativeActivation { #[cfg_attr(not(test), allow(dead_code))] pub(in crate::engine::vm) fn own_continuation(&mut self) -> Result<(), RuntimeError> { - self.active_frame.mark_native_continuation() + self.active_frame + .as_mut() + .expect("native activation lost its active frame") + .mark_native_continuation() } /// Allocate JS engine errors while this native frame and its selected realm @@ -341,7 +370,7 @@ impl NativeActivation { pub(in crate::engine::vm) fn finish_reusing( self, result: Result, - ) -> (Result, Vec) { + ) -> (Result, Vec) { self.finish_reusing_with(result, |value| { NativeInvokeOutcome::Completion(Completion::Throw(value)) }) @@ -350,16 +379,16 @@ impl NativeActivation { pub(in crate::engine::vm) fn finish_completion_reusing( self, result: Result, - ) -> (Result, Vec) { + ) -> (Result, Vec) { self.finish_reusing_with(result, Completion::Throw) } fn finish_reusing_with( - self, + mut self, result: Result, - throw: impl FnOnce(Value) -> T, - ) -> (Result, Vec) { - let runtime = &self.active_frame.runtime; + throw: impl FnOnce(JsValue) -> T, + ) -> (Result, Vec) { + let runtime = self.runtime.clone(); let result = (|| match result { Err(RuntimeError::Engine(error)) if NativeErrorKind::from_javascript_error(error.kind()).is_some() => @@ -367,16 +396,25 @@ impl NativeActivation { let kind = NativeErrorKind::from_javascript_error(error.kind()) .expect("guard proved this is a JavaScript-visible native error"); let value = runtime.new_native_error_from_error(self.realm, kind, &error)?; + let value = runtime.into_jsvalue(value)?; Ok(throw(value)) } result => result, })(); - let result = self.active_frame.finish().and(result); - // Keep the original field cleanup order: the active-frame roots and - // callable owner are released before readable argument owners. - drop(self.callable); - let mut readable = self.arguments.readable; - readable.clear(); + let result = self + .active_frame + .take() + .expect("native activation lost its active frame") + .finish() + .and(result); + // Keep the original field cleanup order: the callable owner is released + // before the readable argument owners. Taking the buffer first leaves + // the activation Drop with nothing to release. + let mut readable = std::mem::take(&mut self.arguments.readable); + drop(self); + for value in readable.drain(..) { + let _ = runtime.release_jsvalue(value); + } (result, readable) } } @@ -412,7 +450,7 @@ mod tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, arguments, NativeInvokeMode::Ordinary, @@ -451,7 +489,9 @@ mod tests { else { panic!("native fixture") }; - let input = Value::Object(runtime.new_object(None).unwrap()); + let input = runtime + .unroot_value(&Value::Object(runtime.new_object(None).unwrap())) + .unwrap(); let invocation = if construct { NativeInvocation::Construct { new_target: input } } else { @@ -468,6 +508,14 @@ mod tests { NativeInvokeMode::Ordinary, ) .unwrap(); + fn native_input(value: &NativeInvocation) -> &JsValue { + match value { + NativeInvocation::Call { this_value } + | NativeInvocation::Getter { this_value } + | NativeInvocation::Setter { this_value } => this_value, + NativeInvocation::Construct { new_target } => new_target, + } + } let borrowed = runtime .adapt_native_invocation_borrowed( target, @@ -477,15 +525,16 @@ mod tests { ) .unwrap(); if fixture == "Reflect.get" { - assert!( - matches!(&borrowed,NativeInvocationAdaptation::Invoke(std::borrow::Cow::Borrowed(value)) if std::ptr::eq(*value,&prepared.invocation)) - ); + let NativeInvocationAdaptation::Invoke(value) = &borrowed else { + panic!("expected invocation adaptation"); + }; + assert_eq!(native_input(value), native_input(&prepared.invocation)); } let owned = runtime .adapt_native_invocation( target, realm, - prepared.invocation.clone(), + prepared.invocation.dup(&runtime).unwrap(), &prepared.activation.arguments, ) .unwrap(); @@ -495,23 +544,23 @@ mod tests { NativeInvocationAdaptation::Invoke(owned), ) => { assert_eq!( - std::mem::discriminant(borrowed.as_ref()), + std::mem::discriminant(&borrowed), std::mem::discriminant(&owned) ); - fn input(value: &NativeInvocation) -> &Value { - match value { - NativeInvocation::Call { this_value } - | NativeInvocation::Getter { this_value } - | NativeInvocation::Setter { this_value } => this_value, - NativeInvocation::Construct { new_target } => new_target, - } - } - assert_eq!(input(borrowed.as_ref()), input(&owned)); + assert_eq!(native_input(&borrowed), native_input(&owned)); } ( - NativeInvocationAdaptation::Complete(Completion::Throw(Value::Object(a))), - NativeInvocationAdaptation::Complete(Completion::Throw(Value::Object(b))), + NativeInvocationAdaptation::Complete(Completion::Throw(throw_a)), + NativeInvocationAdaptation::Complete(Completion::Throw(throw_b)), ) => { + let Value::Object(a) = runtime.root_and_release_jsvalue(throw_a).unwrap() + else { + panic!("expected thrown object"); + }; + let Value::Object(b) = runtime.root_and_release_jsvalue(throw_b).unwrap() + else { + panic!("expected thrown object"); + }; assert_eq!( runtime.get_prototype_of(&a).unwrap(), runtime.get_prototype_of(&b).unwrap() @@ -520,7 +569,7 @@ mod tests { _ => panic!("borrowed and owned adaptation diverged"), } let already_adapted = NativeInvocation::Getter { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }; assert!(matches!( runtime.adapt_native_invocation_borrowed( @@ -551,7 +600,7 @@ mod tests { prepared .activation .finish(Ok(NativeInvokeOutcome::Completion(Completion::Return( - Value::Undefined, + JsValue::Undefined, )))) .unwrap(); assert!(runtime.0.state.borrow().active_frames.is_empty()); @@ -593,7 +642,7 @@ mod tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, arguments, NativeInvokeMode::Ordinary, @@ -666,7 +715,7 @@ mod tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &actual, NativeInvokeMode::Ordinary, @@ -686,7 +735,7 @@ mod tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, owned, NativeInvokeMode::Ordinary, @@ -740,7 +789,7 @@ mod tests { target, min_readable_args + 1, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, vec![], NativeInvokeMode::Ordinary, @@ -756,7 +805,7 @@ mod tests { target, min_readable_args + 1, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, vec![], NativeInvokeMode::Ordinary, @@ -774,14 +823,14 @@ mod tests { ]; let mut errors = Vec::new(); for owned in [false, true] { - let prepared = if owned { + let rejected = if owned { runtime.prepare_native_invocation_owned( callable.clone(), realm, target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, arguments.clone(), NativeInvokeMode::Ordinary, @@ -793,23 +842,13 @@ mod tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &arguments, NativeInvokeMode::Ordinary, ) - } - .unwrap(); - let result = runtime - .dispatch_native_function( - &prepared.activation.callable, - target, - realm, - prepared.invocation, - &prepared.activation.arguments, - ) - .map(NativeInvokeOutcome::Completion); - let error = match prepared.activation.finish(result) { + }; + let error = match rejected { Err(error) => error, Ok(_) => panic!("foreign argument accepted"), }; @@ -894,7 +933,7 @@ mod tests { .arguments .readable .iter() - .all(|value| *value == Value::Undefined) + .all(|value| *value == JsValue::Undefined) ); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || { let _native = native; @@ -919,8 +958,10 @@ mod tests { "activation failure", )))) .unwrap(); - let NativeInvokeOutcome::Completion(Completion::Throw(Value::Object(error))) = result - else { + let NativeInvokeOutcome::Completion(Completion::Throw(thrown)) = result else { + panic!("expected TypeError") + }; + let Value::Object(error) = runtime.root_value(&thrown).unwrap() else { panic!("expected TypeError") }; assert_eq!( @@ -935,17 +976,21 @@ mod tests { }; assert!(stack.to_string().contains("get (native)"), "{stack:?}"); assert!(runtime.0.state.borrow().active_frames.is_empty()); + runtime.release_jsvalue(thrown).unwrap(); let sentinel = runtime.new_object(None).unwrap(); + let sentinel_id = sentinel.object_id(); let native = prepare(&runtime, &mut caller, &[]); let result = native .activation .finish(Ok(NativeInvokeOutcome::Completion(Completion::Throw( - Value::Object(sentinel.clone()), + runtime.unroot_value(&Value::Object(sentinel)).unwrap(), )))) .unwrap(); - assert!( - matches!(result, NativeInvokeOutcome::Completion(Completion::Throw(Value::Object(value))) if value == sentinel) - ); + let NativeInvokeOutcome::Completion(Completion::Throw(value)) = result else { + panic!("expected sentinel throw") + }; + assert_eq!(value, JsValue::Object(sentinel_id)); + runtime.release_jsvalue(value).unwrap(); assert!(runtime.0.state.borrow().active_frames.is_empty()); } @@ -971,7 +1016,7 @@ mod tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &[], NativeInvokeMode::Ordinary, @@ -984,7 +1029,7 @@ mod tests { target, min_readable_args + 1, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &[], NativeInvokeMode::Ordinary, @@ -1030,7 +1075,7 @@ mod continuation_publication_tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, vec![Value::Int(7)], mode, @@ -1094,7 +1139,7 @@ mod continuation_publication_tests { target, min_readable_args.saturating_add(1), NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, Vec::new(), NativeInvokeMode::Ordinary, @@ -1114,7 +1159,7 @@ mod continuation_publication_tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, Vec::new(), NativeInvokeMode::Ordinary, @@ -1274,7 +1319,7 @@ mod publication_witness_tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &[], NativeInvokeMode::Ordinary, @@ -1335,7 +1380,7 @@ mod classified_preparation_tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, vec![Value::Int(3), Value::Int(2)], NativeInvokeMode::Ordinary, @@ -1346,7 +1391,7 @@ mod classified_preparation_tests { prepared .activation .finish(Ok(NativeInvokeOutcome::Completion(Completion::Return( - Value::Int(2), + JsValue::Int(2), )))) .unwrap(); let selection = NativeClassification::select(&runtime, &callable) @@ -1362,7 +1407,7 @@ mod classified_preparation_tests { target, min_readable_args, NativeInvocation::Call { - this_value: Value::Undefined + this_value: JsValue::Undefined }, vec![], NativeInvokeMode::Ordinary, diff --git a/src/engine/vm/call/ordinary.rs b/src/engine/vm/call/ordinary.rs index 341f19d6..4625b318 100644 --- a/src/engine/vm/call/ordinary.rs +++ b/src/engine/vm/call/ordinary.rs @@ -194,8 +194,8 @@ impl OrdinaryCall { pub(in crate::engine::vm) fn prepare_callback( self, storage: &mut crate::engine::vm::frame::CallStorage, - receiver: Value, - arguments: Vec, + receiver: crate::engine::value::JsValue, + arguments: Vec, caller_realm: crate::engine::heap::ContextId, return_to: crate::engine::vm::frame::ReturnTarget, ) -> Result { @@ -204,6 +204,7 @@ impl OrdinaryCall { stack::FrameStorage, }; storage.reserve()?; + let callback_runtime = self.function.runtime().clone(); let (flags, flag_bytes) = if self.executable.has_captured_locals { storage.capture_flags(self.executable.local_definitions.len())? } else { @@ -216,11 +217,12 @@ impl OrdinaryCall { function: self.function.into(), closure_slots: self.closure, reusable_captured_locals: flags, - input: crate::engine::vm::CallInput { - this_value: receiver, - new_target: Value::Undefined, - callee_global: None, - } + input: crate::engine::vm::CallInput::new( + &callback_runtime, + receiver, + crate::engine::value::JsValue::Undefined, + None, + ) .into(), }); #[cfg(feature = "profiling")] @@ -254,7 +256,7 @@ impl OrdinaryCall { pub(in crate::engine::vm) fn install( self, - _runtime: &Runtime, + runtime: &Runtime, execution: &mut crate::engine::vm::execution::RunningExecution, parent: crate::engine::vm::frame::FrameId, count: usize, @@ -271,9 +273,12 @@ impl OrdinaryCall { .checked_add(1) .ok_or_else(|| Error::internal("call resume PC overflow"))?; let receiver = if method { - crate::engine::vm::stack::copy_value(execution.slots.peek(&frame.window, count + 1)?)? + crate::engine::vm::stack::copy_value( + runtime, + execution.slots.peek(&frame.window, count + 1)?, + )? } else { - Value::Undefined + crate::engine::value::JsValue::Undefined }; let (flags, flag_bytes) = if self.executable.has_captured_locals { execution @@ -286,6 +291,7 @@ impl OrdinaryCall { let mut prepared = prepared; let frame = prepared.current_mut(parent)?; let window = execution.slots.push_ordinary_frame( + runtime, &self.executable.frame_layout(), &mut frame.window, count, @@ -306,11 +312,12 @@ impl OrdinaryCall { cold.function = self.function.into(); cold.closure_slots = self.closure; cold.reusable_captured_locals = flags; - cold.input = crate::engine::vm::CallInput { - this_value: receiver, - new_target: Value::Undefined, - callee_global: None, - } + cold.input = crate::engine::vm::CallInput::new( + runtime, + receiver, + crate::engine::value::JsValue::Undefined, + None, + ) .into(); cold.executable = self.executable.into(); cold.window = window.into(); diff --git a/src/engine/vm/call/prepare.rs b/src/engine/vm/call/prepare.rs index 8942e9fe..d26a2b22 100644 --- a/src/engine/vm/call/prepare.rs +++ b/src/engine/vm/call/prepare.rs @@ -7,6 +7,8 @@ use crate::engine::api::runtime_error::RuntimeError; use crate::engine::code::rooted::FunctionBytecodeRef; use crate::engine::code::runtime::{PublishedFunctionData, PublishedFunctionSnapshot}; use crate::engine::object::CallableRef; +use crate::engine::value::JsValue; +#[cfg(test)] use crate::engine::value::Value; use crate::engine::vm::CallInput; use crate::engine::vm::bindings::FrameBinding; @@ -45,15 +47,25 @@ impl Runtime { executable, active_frame, input, - } = self.prepare_bytecode_header(callable, this_value, new_target, bytecode)?; + } = self.prepare_bytecode_header( + callable, + self.unroot_value(&this_value)?, + self.unroot_value(&new_target)?, + bytecode, + )?; let local_definitions = &executable.local_definitions; let metadata = executable.metadata; let argument_slots = executable.frame_layout().argument_slots(arguments.len()); let mut frame_arguments = Vec::new(); let mut frame_locals = Vec::new(); frame_arguments.reserve(argument_slots); - frame_arguments.extend(arguments.iter().cloned().map(FrameBinding::Direct)); - frame_arguments.resize_with(argument_slots, || FrameBinding::Direct(Value::Undefined)); + frame_arguments.extend( + arguments + .iter() + .map(|value| self.unroot_value(value).map(FrameBinding::Direct)) + .collect::, _>>()?, + ); + frame_arguments.resize_with(argument_slots, || FrameBinding::Direct(JsValue::Undefined)); frame_locals.reserve(local_definitions.len()); frame_locals.extend( local_definitions @@ -61,11 +73,13 @@ impl Runtime { .enumerate() .map(|(index, definition)| { initial_local_binding( + self, definition.is_lexical, metadata.function_name_local == Some(index as u16), callable.as_object(), ) - }), + }) + .collect::, _>>()?, ); #[cfg(feature = "profiling")] if crate::engine::api::profiling::cost_profile_active() { @@ -94,8 +108,8 @@ impl Runtime { pub(in crate::engine::vm) fn prepare_owned_bytecode_frame( &self, callable: &CallableRef, - this_value: Value, - new_target: Value, + this_value: JsValue, + new_target: JsValue, bytecode: FunctionBytecodeRef, ) -> Result { #[cfg(feature = "profiling")] @@ -107,8 +121,8 @@ impl Runtime { fn prepare_bytecode_header( &self, callable: &CallableRef, - this_value: Value, - new_target: Value, + this_value: JsValue, + new_target: JsValue, bytecode: FunctionBytecodeRef, ) -> Result { let executable = self.snapshot_function_bytecode(&bytecode)?; @@ -138,26 +152,26 @@ impl Runtime { Ok(PreparedBytecodeHeader { executable, active_frame, - input: CallInput { - this_value, - new_target, - callee_global: Some(callee_global), - }, + input: CallInput::new(self, this_value, new_target, Some(callee_global)), }) } } /// Shared initial binding shape for both legacy vectors and owned windows. +/// The named-function binding duplicates the callable's edge for the frame. pub(in crate::engine::vm) fn initial_local_binding( + runtime: &Runtime, lexical: bool, function_name: bool, callable: &crate::engine::object::ObjectRef, -) -> FrameBinding { +) -> Result { if function_name { - FrameBinding::Direct(Value::Object(callable.clone())) + let id = callable.object_id(); + runtime.retain_object_handle(id)?; + Ok(FrameBinding::Direct(JsValue::Object(id))) } else if lexical { - FrameBinding::Uninitialized + Ok(FrameBinding::Uninitialized) } else { - FrameBinding::Direct(Value::Undefined) + Ok(FrameBinding::Direct(JsValue::Undefined)) } } diff --git a/src/engine/vm/call/prototype.rs b/src/engine/vm/call/prototype.rs index d0b9363c..dd393e81 100644 --- a/src/engine/vm/call/prototype.rs +++ b/src/engine/vm/call/prototype.rs @@ -3,14 +3,14 @@ use super::ConstructorPrototypeSource; use crate::engine::{ api::{runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, - object::PropertyKey, - value::{Value, conversion::NativeConversion}, + object::{ObjectRef, PropertyKey}, + value::{JsValue, Value, conversion::NativeConversion}, vm::Completion, }; pub(crate) enum ProtoSourceStep { Complete(NativeConversion), ReadValue { - receiver: Value, + receiver: JsValue, key: PropertyKey, resume: ProtoSourceResume, }, @@ -44,7 +44,7 @@ impl ProtoSourceStep { ))); } Ok(Self::ReadValue { - receiver: new_target.clone(), + receiver: runtime.unroot_value(&new_target)?, key: runtime.pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype)?, resume: ProtoSourceResume(Box::new(ProtoSourceResumeState { realm, new_target })), }) @@ -57,9 +57,13 @@ impl ProtoSourceResume { reply: Completion, ) -> Result { let result = match reply { - Completion::Throw(value) => NativeConversion::Throw(value), - Completion::Return(Value::Object(prototype)) => { - NativeConversion::Value(ConstructorPrototypeSource::Explicit(prototype)) + Completion::Throw(value) => { + NativeConversion::Throw(runtime.root_and_release_jsvalue(value)?) + } + Completion::Return(JsValue::Object(prototype)) => { + NativeConversion::Value(ConstructorPrototypeSource::Explicit( + ObjectRef::from_owned_handle(runtime.clone(), prototype), + )) } Completion::Return(_) => { match runtime.function_realm_from_value(self.0.realm, &self.0.new_target)? { @@ -87,7 +91,11 @@ pub(crate) fn finish( resume, } => resume.resume( runtime, - runtime.get_value_property_in_realm(realm, receiver, &key)?, + runtime.get_value_property_in_realm( + realm, + runtime.root_and_release_jsvalue(receiver)?, + &key, + )?, )?, }; } diff --git a/src/engine/vm/call/request.rs b/src/engine/vm/call/request.rs index 5bd8a80d..e8b2f8f9 100644 --- a/src/engine/vm/call/request.rs +++ b/src/engine/vm/call/request.rs @@ -6,7 +6,7 @@ use crate::engine::api::runtime::Runtime; use crate::engine::code::rooted::FunctionBytecodeRef; use crate::engine::heap::ContextId; use crate::engine::object::CallableRef; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::exception::runtime_error_to_vm_error; use crate::engine::vm::frame::{FrameCold, FrameEntry, ReturnTarget}; use crate::engine::vm::stack::FrameStorage; @@ -15,9 +15,9 @@ use crate::engine::vm::stack::FrameStorage; /// this request. Its values stay rooted independently of the parent window. pub(in crate::engine::vm) struct BytecodeCallRequest { pub callable: CallableRef, - pub receiver: Value, - pub new_target: Value, - pub arguments: Vec, + pub receiver: JsValue, + pub new_target: JsValue, + pub arguments: Vec, pub bytecode: FunctionBytecodeRef, pub closure_slots: crate::engine::vm::closure::ClosureSlots, pub caller_realm: ContextId, @@ -102,8 +102,8 @@ impl BytecodeCallRequest { /// and innermost bound receiver before choosing their driver entry. pub(in crate::engine::vm) struct NormalizedCallback { pub callable: CallableRef, - pub receiver: Value, - pub arguments: Vec, + pub receiver: JsValue, + pub arguments: Vec, pub classification: super::CallableExecution, } @@ -125,17 +125,37 @@ pub(in crate::engine::vm) fn normalize_callback( this_value, arguments: bound, } => { + let bound = bound + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; arguments = match runtime .concatenate_bound_arguments(realm, &bound, &arguments) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(arguments) => arguments, - NativeConversion::Throw(value) => return Ok(NativeConversion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(NativeConversion::Throw(value)); + } }; - receiver = this_value; + receiver = runtime + .root_and_release_jsvalue(this_value) + .map_err(runtime_error_to_vm_error)?; callable = target; } classification => { + // The normalized owners enter the internal call convention: + // their root edges are duplicated; the caller's roots release + // through the public Drop path. + let receiver = runtime + .unroot_value(&receiver) + .map_err(runtime_error_to_vm_error)?; + let arguments = arguments + .iter() + .map(|argument| runtime.unroot_value(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; return Ok(NativeConversion::Value(NormalizedCallback { callable, receiver, diff --git a/src/engine/vm/closure.rs b/src/engine/vm/closure.rs index 323d7d5e..03fa1983 100644 --- a/src/engine/vm/closure.rs +++ b/src/engine/vm/closure.rs @@ -55,6 +55,7 @@ mod tests { use crate::engine::{ api::{Runtime, Value}, object::CallableRef, + value::JsValue, vm::call::CallableExecution, }; #[test] @@ -118,20 +119,20 @@ mod tests { runtime .read_var_ref(&closure_slots.get(0).unwrap()) .unwrap(), - Value::Int(1) + JsValue::Int(1) ); runtime - .write_var_ref(&closure_slots.get(0).unwrap(), Value::Int(9)) + .write_var_ref(&closure_slots.get(0).unwrap(), JsValue::Int(9)) .unwrap(); assert_eq!( runtime .read_var_ref(&closure_slots.get(0).unwrap()) .unwrap(), - Value::Int(9) + JsValue::Int(9) ); let escaped = closure_slots.get(0).unwrap().clone(); drop(closure_slots); - assert_eq!(runtime.read_var_ref(&escaped).unwrap(), Value::Int(9)); + assert_eq!(runtime.read_var_ref(&escaped).unwrap(), JsValue::Int(9)); assert!(runtime.0.state.borrow().heap.var_ref(ids[1]).is_err()); drop(escaped); assert!(runtime.0.state.borrow().heap.var_ref(ids[0]).is_err()); diff --git a/src/engine/vm/closure_driver.rs b/src/engine/vm/closure_driver.rs index 7a0e3c6f..22ba5a9d 100644 --- a/src/engine/vm/closure_driver.rs +++ b/src/engine/vm/closure_driver.rs @@ -86,9 +86,10 @@ pub(super) fn instantiate( .map_err(runtime_error_to_vm_error)?; #[cfg(feature = "profiling")] let depth = execution.slots.depth(&frame.window); - execution - .slots - .push(&mut frame.window, Value::Object(callable.into_object()))?; + let value = runtime + .into_jsvalue(Value::Object(callable.into_object())) + .map_err(runtime_error_to_vm_error)?; + execution.slots.push(&mut frame.window, value)?; frame.resume_pc = frame .fault_pc .checked_add(1) diff --git a/src/engine/vm/completion.rs b/src/engine/vm/completion.rs index 6987f63e..a807d312 100644 --- a/src/engine/vm/completion.rs +++ b/src/engine/vm/completion.rs @@ -1,12 +1,12 @@ -use crate::engine::value::Value; +use crate::engine::value::JsValue; -/// Private JavaScript control completion. A thrown value remains a rooted -/// ordinary [`Value`]; no exception sentinel is exposed through the public +/// Private JavaScript control completion. A thrown value remains an owned +/// internal [`JsValue`]; no exception sentinel is exposed through the public /// value representation. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub(crate) enum Completion { - Return(Value), - Throw(Value), + Return(JsValue), + Throw(JsValue), } /// The suspension sites used by QuickJS generator and async-function bytecode. @@ -34,11 +34,11 @@ pub(crate) enum VmSuspendKind { /// exception: it is raised through the activation's normal unwind machinery, /// while `yield*` receives value + magic 2 so the compiled delegation loop can /// invoke the delegate's `throw` method. -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug)] pub(crate) enum VmResume { - Next(Value), - Return(Value), - Throw(Value), + Next(JsValue), + Return(JsValue), + Throw(JsValue), } /// Result of QuickJS `OP_define_class` at the VM/runtime boundary. @@ -46,13 +46,13 @@ pub(crate) enum VmResume { /// A successful definition replaces the two input operands with two freshly /// published outputs. JavaScript-visible failures stay typed as thrown values /// so an enclosing bytecode catch region can handle them normally. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub(crate) enum DefineClassOutcome { Defined { - constructor: Value, - prototype: Value, + constructor: JsValue, + prototype: JsValue, }, - Throw(Value), + Throw(JsValue), } /// ECMAScript ToPrimitive hint crossing the VM/runtime host boundary. diff --git a/src/engine/vm/construct_driver.rs b/src/engine/vm/construct_driver.rs index 2c70a747..9388b0a7 100644 --- a/src/engine/vm/construct_driver.rs +++ b/src/engine/vm/construct_driver.rs @@ -3,8 +3,8 @@ //! use the same owned property query as other Get operations. use crate::engine::api::{error::Error, runtime::Runtime}; use crate::engine::code::function::metadata::FunctionKind; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::Completion; use crate::engine::vm::call::{BytecodeCallRequest, CallableExecution}; use crate::engine::vm::driver::{CallStep, push_frame}; @@ -22,23 +22,36 @@ pub(super) fn enter( let count = usize::from(count); let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; - let target = execution.slots.peek(&frame.window, count + 1)?.clone(); + // The constructor classifier consumes a public root; the slot owner keeps + // its own edge until `start_construct` consumes the operands. + let target = runtime + .root_value(execution.slots.peek(&frame.window, count + 1)?) + .map_err(runtime_error_to_vm_error)?; let constructor = match runtime.constructor_from_value(realm, target) { Ok(NativeConversion::Value(target)) => target, Ok(NativeConversion::Throw(value)) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(CallStep::Complete(Completion::Throw(value))); } Err(error) => { return super::driver::rejected_call(runtime, realm, runtime_error_to_vm_error(error)); } }; - let new_target = execution.slots.peek(&frame.window, count)?.clone(); + let new_target = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, count)?) + .map_err(runtime_error_to_vm_error)?; let mut arguments = Vec::new(); arguments .try_reserve_exact(count) .map_err(|_| Error::internal("construct arguments allocation failed"))?; for offset in (0..count).rev() { - arguments.push(execution.slots.peek(&frame.window, offset)?.clone()); + arguments.push( + runtime + .dup_jsvalue(execution.slots.peek(&frame.window, offset)?) + .map_err(runtime_error_to_vm_error)?, + ); } super::proxy_get_driver::start_construct( runtime, @@ -59,7 +72,7 @@ pub(super) fn enter_default_derived( ) -> Result { let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; - if matches!(frame.cold.input.new_target, Value::Undefined) { + if matches!(frame.cold.input.new_target, JsValue::Undefined) { return super::driver::rejected_call( runtime, realm, @@ -78,10 +91,15 @@ pub(super) fn enter_default_derived( let arguments = execution .slots .snapshot_actual_arguments(&frame.window, runtime)?; - let new_target = frame.cold.input.new_target.clone(); + let new_target = runtime + .dup_jsvalue(&frame.cold.input.new_target) + .map_err(runtime_error_to_vm_error)?; let constructor = match runtime.constructor_from_value(realm, target) { Ok(NativeConversion::Value(constructor)) => constructor, Ok(NativeConversion::Throw(value)) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(CallStep::Complete(Completion::Throw(value))); } Err(error) => { @@ -127,44 +145,71 @@ pub(super) fn initializer( runtime .install_class_instance_initializer( realm, - execution.slots.peek(&frame.window, 2)?.clone(), - execution.slots.peek(&frame.window, 1)?.clone(), - execution.slots.peek(&frame.window, 0)?.clone(), + runtime + .root_value(execution.slots.peek(&frame.window, 2)?) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_value(execution.slots.peek(&frame.window, 1)?) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)?; - (None, Value::Undefined) + (None, JsValue::Undefined) } InitializerKind::Instance => { - let receiver = execution.slots.peek(&frame.window, 1)?.clone(); + let receiver = runtime + .root_value(execution.slots.peek(&frame.window, 1)?) + .map_err(runtime_error_to_vm_error)?; let callable = runtime .begin_class_instance_initializer( realm, - execution.slots.peek(&frame.window, 0)?.clone(), + runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?, &receiver, ) .map_err(runtime_error_to_vm_error)?; + let receiver = runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; (callable, receiver) } InitializerKind::Static => { let (callable, receiver) = runtime .begin_class_static_initializer( realm, - execution.slots.peek(&frame.window, 1)?.clone(), - execution.slots.peek(&frame.window, 0)?.clone(), + runtime + .root_value(execution.slots.peek(&frame.window, 1)?) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)?; + let receiver = runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; (Some(callable), receiver) } InitializerKind::Block => { - let receiver = frame.cold.input.this_value.clone(); + let receiver = runtime + .root_value(&frame.cold.input.this_value) + .map_err(runtime_error_to_vm_error)?; let callable = runtime .begin_class_static_block( realm, &frame.cold.function, &receiver, - execution.slots.peek(&frame.window, 0)?.clone(), + runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)?; + let receiver = runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; (Some(callable), receiver) } }; @@ -194,10 +239,19 @@ pub(super) fn initializer( "authenticated class initializer is not ordinary bytecode", )); } + if !execution.frames.can_push() || runtime.bytecode_call_would_overflow() { + runtime + .release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + return runtime + .bytecode_stack_overflow_completion(realm, &bytecode) + .map(CallStep::Complete) + .map_err(runtime_error_to_vm_error); + } Some(BytecodeCallRequest { callable, receiver, - new_target: Value::Undefined, + new_target: JsValue::Undefined, arguments: Vec::new(), bytecode, closure_slots, @@ -210,22 +264,20 @@ pub(super) fn initializer( }, }) } else { + runtime + .release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; None }; - if let Some(request) = &request { - if !execution.frames.can_push() || runtime.bytecode_call_would_overflow() { - return runtime - .bytecode_stack_overflow_completion(realm, &request.bytecode) - .map(CallStep::Complete) - .map_err(runtime_error_to_vm_error); - } - } let frame = execution.frames.current_mut(id)?; - execution.slots.pop(&mut frame.window)?; + let discarded = execution.slots.pop(&mut frame.window)?; frame.resume_pc = frame .fault_pc .checked_add(1) .ok_or_else(|| Error::internal("initializer resume PC overflow"))?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; if let Some(request) = request { let entry = request.prepare(runtime, &mut execution.call_storage)?; push_frame(execution, entry)?; @@ -243,7 +295,7 @@ pub(super) fn initializer( }; Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -266,25 +318,38 @@ pub(super) fn define_class( let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; let parent = execution.slots.peek(&frame.window, 1)?; - let Some(BytecodeConstant::Value(RawValue::String(name))) = frame.executable.constant(name) + let Some(BytecodeConstant::Value(RawValue::String(name_id))) = frame.executable.constant(name) else { return Err(Error::internal("class name is not a published string")); }; - if has_heritage && let Value::Object(parent) = parent { + // The bytecode node owns the constant-pool edge for this string, so the + // trusted read clones the payload Rc without retaining the arena node. + let name = runtime.0.state.borrow().heap.string_fast(*name_id).clone(); + if has_heritage && let JsValue::Object(parent) = parent { let pending = PendingClass { frame: id, realm, - parent: parent.clone(), - constructor: execution.slots.peek(&frame.window, 0)?.clone(), + parent: crate::engine::object::ObjectRef::from_borrowed_handle( + runtime.clone(), + *parent, + ) + .map_err(super::exception::heap_error_to_vm_error)?, + constructor: runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?, name: name.clone(), }; return enter_class_parent(runtime, execution, pending); } let result = runtime.define_class_pair( realm, - parent.clone(), - execution.slots.peek(&frame.window, 0)?.clone(), - name, + runtime + .root_value(parent) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?, + &name, has_heritage, ); finish_class_result(runtime, execution, id, result) @@ -305,8 +370,12 @@ fn finish_class_result( #[cfg(feature = "profiling")] let depth = execution.slots.depth(&frame.window); // No replay once the fresh constructor/prototype pair is published. - execution.slots.pop(&mut frame.window)?; - execution.slots.pop(&mut frame.window)?; + for _ in 0..2 { + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; + } frame.resume_pc = frame .fault_pc .checked_add(1) @@ -332,7 +401,7 @@ fn finish_class_result( }; Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -343,7 +412,7 @@ pub(super) struct PendingClass { pub(super) frame: FrameId, pub(super) realm: crate::engine::heap::ContextId, pub(super) parent: crate::engine::object::ObjectRef, - constructor: Value, + constructor: JsValue, name: crate::engine::value::JsString, } @@ -379,10 +448,14 @@ pub(super) fn finish_class_reply( Completion::Throw(value) => Ok(crate::engine::vm::DefineClassOutcome::Throw(value)), Completion::Return(prototype) => runtime.finish_derived_class_pair( pending.realm, - pending.constructor, + runtime + .root_and_release_jsvalue(pending.constructor) + .map_err(runtime_error_to_vm_error)?, &pending.name, pending.parent, - prototype, + runtime + .root_and_release_jsvalue(prototype) + .map_err(runtime_error_to_vm_error)?, ), }; finish_class_result(runtime, execution, pending.frame, result) @@ -401,7 +474,7 @@ pub(super) fn define_property( use crate::engine::object::PropertyKey; let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; - let Value::Object(object) = execution + let JsValue::Object(object) = execution .slots .peek(&frame.window, 1 + usize::from(key.is_none()))? else { @@ -437,32 +510,46 @@ pub(super) fn define_property( }; let depth = execution.slots.depth(&frame.window); if method.is_none() { - let object = object.clone(); + let object = + crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), *object) + .map_err(super::exception::heap_error_to_vm_error)?; let value = execution.slots.pop(&mut frame.window)?; if computed { - execution.slots.pop(&mut frame.window)?; + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; } return super::proxy_get_driver::start_public_field( runtime, execution, id, object, key, value, depth, ); } let (kind, enumerable) = method.expect("method checked above"); - let object = object.clone(); - let descriptor = - match runtime.prepare_object_literal_method(&object, &key, value.clone(), kind, enumerable) - { - Ok(descriptor) => descriptor, - Err(error) => { - return super::driver::rejected_call( - runtime, - realm, - runtime_error_to_vm_error(error), - ); - } - }; - execution.slots.pop(&mut frame.window)?; + let object = crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), *object) + .map_err(super::exception::heap_error_to_vm_error)?; + let descriptor = match runtime.prepare_object_literal_method( + &object, + &key, + runtime + .root_value(value) + .map_err(runtime_error_to_vm_error)?, + kind, + enumerable, + ) { + Ok(descriptor) => descriptor, + Err(error) => { + return super::driver::rejected_call(runtime, realm, runtime_error_to_vm_error(error)); + } + }; + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; if computed { - execution.slots.pop(&mut frame.window)?; + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; } super::proxy_get_driver::start_literal_definition( runtime, diff --git a/src/engine/vm/conversion_driver.rs b/src/engine/vm/conversion_driver.rs index fad0bbcc..05c0a183 100644 --- a/src/engine/vm/conversion_driver.rs +++ b/src/engine/vm/conversion_driver.rs @@ -4,8 +4,8 @@ mod local_add; use crate::engine::api::{error::Error, runtime::Runtime}; use crate::engine::code::function::metadata::FunctionKind; use crate::engine::object::{CallableRef, OrdinaryRead}; -use crate::engine::value::Value; use crate::engine::value::conversion::primitive::{PrimitiveResume, PrimitiveStep}; +use crate::engine::value::{JsValue, Value}; use crate::engine::vm::call::{BytecodeCallRequest, CallableExecution}; use crate::engine::vm::exception::runtime_error_to_vm_error; use crate::engine::vm::execution::RunningExecution; @@ -19,28 +19,53 @@ enum Finish { Plus, PropertyKey, PropertyWrite { - base: Value, - value: Value, + base: JsValue, + value: JsValue, }, PropertyRead { - base: Value, + base: JsValue, keep_receiver: bool, keep_key: bool, }, - AddLeft(Value), - AddRight(Value), + AddLeft(JsValue), + AddRight(JsValue), } /// The same resident state moves between task and wait as one pointer. pub(super) struct ConversionWait(ConversionTask); pub(super) struct ConversionTask(Option>); pub(super) struct ConversionState { + runtime: Runtime, finish: Finish, frame: FrameId, identity: u64, step: Option, resume: Option, } +impl Drop for ConversionState { + /// Release the internal edges the abandoned conversion still owns. + /// Consumption uses `Option::take`/`mem::replace`, so a drained slot is + /// `Undefined` here; releases are defer-safe and nothrow. + fn drop(&mut self) { + let finish = std::mem::replace(&mut self.finish, Finish::Plus); + match finish { + Finish::PropertyWrite { base, value } => { + let _ = self.runtime.release_jsvalue(base); + let _ = self.runtime.release_jsvalue(value); + } + Finish::PropertyRead { base, .. } => { + let _ = self.runtime.release_jsvalue(base); + } + Finish::AddLeft(value) | Finish::AddRight(value) => { + let _ = self.runtime.release_jsvalue(value); + } + Finish::Predicate(_) + | Finish::SuperProperty(_) + | Finish::Plus + | Finish::PropertyKey => {} + } + } +} impl std::ops::Deref for ConversionTask { type Target = ConversionState; fn deref(&self) -> &Self::Target { @@ -75,28 +100,24 @@ pub(super) enum Progress { PropertyWrite(Box), } -fn property_key_primitive(runtime: &Runtime, value: Value) -> Result { +fn property_key_primitive(runtime: &Runtime, value: JsValue) -> Result { Ok(match value { - Value::Symbol(symbol) => { - if !symbol.belongs_to(runtime) { - return Err(Error::internal( - "computed property symbol belongs to another runtime", - )); - } - Value::Symbol(symbol) - } - Value::String(string) => Value::String(string), - primitive => Value::String(primitive.to_js_string()?), + JsValue::Symbol(_) => value, + JsValue::String(_) => value, + primitive => super::numeric::allocate_string_jsvalue( + runtime, + super::numeric::to_js_string_jsvalue(runtime, &primitive)?, + )?, }) } fn add_completion( runtime: &Runtime, realm: crate::engine::heap::ContextId, - left: Value, - right: Value, + left: JsValue, + right: JsValue, ) -> Result { - match super::numeric::add_primitives(left, right) { + match super::numeric::add_primitives(runtime, left, right) { Ok(value) => Ok(Completion::Return(value)), Err(error) => { let Some(kind) = @@ -106,7 +127,7 @@ fn add_completion( }; Ok(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, )) } @@ -115,7 +136,7 @@ fn add_completion( pub(super) enum PrimitiveCompletion { Completed, - Throw(Value), + Throw(JsValue), Declined, } @@ -137,19 +158,12 @@ pub(super) fn complete_primitives( let mut transaction = execution.slots.frame_transaction(&mut body.window)?; let (left, right, store) = { let mut slots = transaction.slots(); - // Preserve left-to-right domain validation, including checking a later - // malformed slot after an earlier invalid domain, before identity issue. - let mut invalid = None; + // Internal values carry no runtime branding; authenticate every operand + // slot in the original left-to-right order before the identity issue. let mut has_object = false; for offset in (0..=usize::from(addition)).rev() { let value = slots.peek(offset)?; - if let Err(error) = runtime.validate_value_domain(value, "conversion operand") { - invalid.get_or_insert(error); - } - has_object |= matches!(value, Value::Object(_)); - } - if let Some(error) = invalid { - return Err(super::exception::runtime_error_to_vm_error(error)); + has_object |= matches!(value, JsValue::Object(_)); } *next_operation = next_operation .checked_add(1) @@ -166,7 +180,7 @@ pub(super) fn complete_primitives( super::bindings::FrameBinding::Direct(value) => Some(( *index, executable.fusion.add_store_span(frame.fault_pc), - matches!(value, Value::Object(_) | Value::Symbol(_)), + matches!(value, JsValue::Object(_) | JsValue::Symbol(_)), )), _ => None, }, @@ -194,7 +208,7 @@ pub(super) fn complete_primitives( let completion = if let Some(left) = left { add_completion(runtime, realm, left, right)? } else { - match super::numeric::unary_plus_primitive(right) { + match super::numeric::unary_plus_primitive(runtime, right) { Ok(value) => Completion::Return(value), Err(error) => { let Some(kind) = @@ -204,7 +218,7 @@ pub(super) fn complete_primitives( }; Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ) } @@ -253,7 +267,7 @@ pub(super) fn complete_primitives( return Err(error); } }; - drop(old); + super::bindings::release_frame_binding(runtime, old)?; // The optional Drop only removes the assignment result while // the local keeps the value; it has no observable owner drain. let resume = store_pc @@ -292,10 +306,17 @@ pub(super) fn complete_primitives( impl ConversionTask { #[inline(always)] - fn new(finish: Finish, frame: FrameId, identity: u64, step: PrimitiveStep) -> Self { + fn new( + runtime: &Runtime, + finish: Finish, + frame: FrameId, + identity: u64, + step: PrimitiveStep, + ) -> Self { #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event("conversion_task_allocated"); Self(Some(Box::new(ConversionState { + runtime: runtime.clone(), finish, frame, identity, @@ -338,7 +359,7 @@ impl ConversionTask { ) -> Result { let parent = execution.frames.current_mut(frame)?; let right = execution.slots.pop(&mut parent.window)?; - if property_key && !addition && !matches!(right, Value::Object(_)) { + if property_key && !addition && !matches!(right, JsValue::Object(_)) { let value = property_key_primitive(runtime, right)?; #[cfg(feature = "profiling")] let depth = execution.slots.depth(&parent.window) + 1; @@ -363,6 +384,7 @@ impl ConversionTask { (right, Finish::Plus, ToPrimitiveHint::Number) }; Ok(Self::new( + runtime, finish, frame, identity, @@ -378,9 +400,16 @@ impl ConversionTask { input: Box, ) -> Result { let realm = execution.frames.current_mut(frame)?.executable.realm; - let step = - PrimitiveResume::start(runtime, realm, input.key.clone(), ToPrimitiveHint::String); + let step = PrimitiveResume::start( + runtime, + realm, + runtime + .unroot_value(&input.key) + .map_err(runtime_error_to_vm_error)?, + ToPrimitiveHint::String, + ); Ok(Self::new( + runtime, Finish::Predicate(Some(input)), frame, identity, @@ -396,9 +425,16 @@ impl ConversionTask { input: Box, ) -> Result { let realm = execution.frames.current_mut(frame)?.executable.realm; - let step = - PrimitiveResume::start(runtime, realm, input.key.clone(), ToPrimitiveHint::String); + let step = PrimitiveResume::start( + runtime, + realm, + runtime + .dup_jsvalue(&input.key) + .map_err(runtime_error_to_vm_error)?, + ToPrimitiveHint::String, + ); Ok(Self::new( + runtime, Finish::SuperProperty(Some(input)), frame, identity, @@ -413,19 +449,11 @@ impl ConversionTask { identity: u64, ) -> Result { let parent = execution.frames.current_mut(frame)?; - for (offset, label) in [ - (2, "property receiver"), - (1, "property key"), - (0, "property value"), - ] { - runtime - .validate_value_domain(execution.slots.peek(&parent.window, offset)?, label) - .map_err(runtime_error_to_vm_error)?; - } let value = execution.slots.pop(&mut parent.window)?; let key = execution.slots.pop(&mut parent.window)?; let base = execution.slots.pop(&mut parent.window)?; Ok(Self::new( + runtime, Finish::PropertyWrite { base, value }, frame, identity, @@ -447,18 +475,12 @@ impl ConversionTask { keep_key: bool, ) -> Result { let parent = execution.frames.current_mut(frame)?; - runtime - .validate_value_domain( - execution.slots.peek(&parent.window, 1)?, - "property receiver", - ) - .map_err(runtime_error_to_vm_error)?; - runtime - .validate_value_domain(execution.slots.peek(&parent.window, 0)?, "property key") - .map_err(runtime_error_to_vm_error)?; + execution.slots.peek(&parent.window, 1)?; + execution.slots.peek(&parent.window, 0)?; let key = execution.slots.pop(&mut parent.window)?; let base = execution.slots.pop(&mut parent.window)?; Ok(Self::new( + runtime, Finish::PropertyRead { base, keep_receiver, @@ -534,13 +556,13 @@ impl ConversionTask { Completion::Return(value) => { // Domain completion guarantees a primitive: this call // cannot recursively perform another ToPrimitive. - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { return Err(Error::internal("conversion returned an object")); } match &mut self.finish { Finish::AddLeft(right) => { - let right = std::mem::replace(right, Value::Undefined); - if !matches!(right, Value::Object(_)) { + let right = std::mem::replace(right, JsValue::Undefined); + if !matches!(right, JsValue::Object(_)) { #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event( "add_completed_with_primitive_rhs", @@ -562,12 +584,14 @@ impl ConversionTask { Finish::AddRight(left) => add_completion( runtime, realm, - std::mem::replace(left, Value::Undefined), + std::mem::replace(left, JsValue::Undefined), value, )?, Finish::Predicate(input) => { let mut input = input.take().expect("predicate conversion input"); - input.key = value; + input.key = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(Progress::Predicate(input)); } Finish::SuperProperty(input) => { @@ -579,13 +603,19 @@ impl ConversionTask { base, value: assigned, } => { - let base = std::mem::replace(base, Value::Undefined); - let assigned = std::mem::replace(assigned, Value::Undefined); + let base = std::mem::replace(base, JsValue::Undefined); + let assigned = std::mem::replace(assigned, JsValue::Undefined); return Ok(Progress::PropertyWrite(Box::new( super::property_write_driver::ConvertedWrite { - base, - key: value, - value: assigned, + base: runtime + .root_and_release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?, + key: runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + value: runtime + .root_and_release_jsvalue(assigned) + .map_err(runtime_error_to_vm_error)?, }, ))); } @@ -594,7 +624,7 @@ impl ConversionTask { keep_receiver, keep_key, } => { - let base = std::mem::replace(base, Value::Undefined); + let base = std::mem::replace(base, JsValue::Undefined); let keep_receiver = *keep_receiver; let keep_key = *keep_key; return Ok(Progress::PropertyRead(Box::new( @@ -609,17 +639,21 @@ impl ConversionTask { Finish::PropertyKey => { Completion::Return(property_key_primitive(runtime, value)?) } - Finish::Plus => match super::numeric::unary_plus_primitive(value) { - Ok(value) => Completion::Return(value), - Err(error) => { - let Some(kind) = crate::engine::api::error::NativeErrorKind::from_javascript_error(error.kind()) else { return Err(error); }; - Completion::Throw( - runtime - .new_native_error_from_error(realm, kind, &error) - .map_err(runtime_error_to_vm_error)?, - ) + Finish::Plus => { + match super::numeric::unary_plus_primitive(runtime, value) { + Ok(value) => Completion::Return(value), + Err(error) => { + let Some(kind) = crate::engine::api::error::NativeErrorKind::from_javascript_error(error.kind()) else { return Err(error); }; + Completion::Throw( + runtime + .new_native_error_from_error_jsvalue( + realm, kind, &error, + ) + .map_err(runtime_error_to_vm_error)?, + ) + } } - }, + } } } }; @@ -645,7 +679,7 @@ impl ConversionTask { resume .resume( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), ) .map_err(runtime_error_to_vm_error)?, ), @@ -679,8 +713,18 @@ impl ConversionTask { } PrimitiveStep::Call { mut resume } => { let callable = resume.take_callable(); - let receiver = resume.take_receiver(); - let arguments = resume.take_arguments(); + // `normalize_callback` still crosses on public roots; the + // conversion domain stores internal values, so hand the + // callback boundary owned roots and let it re-enter. + let receiver = runtime + .root_and_release_jsvalue(resume.take_receiver()) + .map_err(runtime_error_to_vm_error)?; + let arguments = resume + .take_arguments() + .into_iter() + .map(|value| runtime.root_and_release_jsvalue(value)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; invoke( runtime, execution, self, callable, receiver, arguments, resume, ) @@ -710,6 +754,11 @@ fn invoke( } = match super::call::normalize_callback(runtime, realm, callable, receiver, arguments)? { crate::engine::value::conversion::NativeConversion::Value(call) => call, crate::engine::value::conversion::NativeConversion::Throw(value) => { + // The callback boundary threw a public root; transfer it into the + // internal completion without a retain/release pair. + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(Progress::Ready( task.with_step( resume @@ -796,7 +845,7 @@ fn invoke( callable, receiver, arguments, - new_target: Value::Undefined, + new_target: JsValue::Undefined, bytecode, closure_slots, caller_realm: realm, diff --git a/src/engine/vm/conversion_driver/local_add.rs b/src/engine/vm/conversion_driver/local_add.rs index e37c8ae3..f4708251 100644 --- a/src/engine/vm/conversion_driver/local_add.rs +++ b/src/engine/vm/conversion_driver/local_add.rs @@ -19,8 +19,8 @@ pub(in crate::engine::vm) fn complete_local_add( // the constant as the left operand so concatenation order cannot swap. enum Operands { Locals(u16, u16), - LocalConstant(Value), - ConstantLocal(Value), + LocalConstant(JsValue), + ConstantLocal(JsValue), } let (store, operands, prepend) = match &frame.executable.code[start..] { [ @@ -33,7 +33,7 @@ pub(in crate::engine::vm) fn complete_local_add( Instruction::PushConst(index), .., ] => { - let Some(constant) = constant_string(&frame.executable, *index) else { + let Some(constant) = constant_string(runtime, &frame.executable, *index) else { return Ok(PrimitiveCompletion::Declined); }; (*left, Operands::LocalConstant(constant), false) @@ -43,7 +43,7 @@ pub(in crate::engine::vm) fn complete_local_add( Instruction::GetLocal(right) | Instruction::GetLocalCheck(right), .., ] => { - let Some(constant) = constant_string(&frame.executable, *index) else { + let Some(constant) = constant_string(runtime, &frame.executable, *index) else { return Ok(PrimitiveCompletion::Declined); }; (*right, Operands::ConstantLocal(constant), true) @@ -56,11 +56,11 @@ pub(in crate::engine::vm) fn complete_local_add( // All preflight remains non-mutating; checked/captured/TDZ fallbacks retain // the canonical first operand PC and original operand stack. enum PreparedAdd { - Exhausted(Value, Value), - Result(Result), + Exhausted(JsValue, JsValue), + Result(Result), Appended, } - let consume = |left: &mut Value, right: &Value| { + let consume = |left: &mut JsValue, right: &JsValue| { frame.fault_pc = start + 2; frame.resume_pc = frame.fault_pc; #[cfg(feature = "profiling")] @@ -70,48 +70,84 @@ pub(in crate::engine::vm) fn complete_local_add( } let Some(next) = next_operation.checked_add(1) else { // Reconstruct canonical operands only on this cold error. - return Ok::<_, Error>(PreparedAdd::Exhausted(left.clone(), right.clone())); + return Ok::<_, Error>(PreparedAdd::Exhausted( + runtime + .dup_jsvalue(left) + .map_err(runtime_error_to_vm_error)?, + runtime + .dup_jsvalue(right) + .map_err(runtime_error_to_vm_error)?, + )); }; *next_operation = next; - if let Value::String(string) = left { + if let JsValue::String(id) = left { let suffix = match right { - Value::String(value) => std::borrow::Cow::Borrowed(value), - value => match value.to_js_string() { - Ok(value) => std::borrow::Cow::Owned(value), - Err(error) => return Ok(PreparedAdd::Result(Err(error))), + JsValue::String(right) => super::super::numeric::string_payload(runtime, *right)?, + value => match super::super::numeric::to_js_string_jsvalue(runtime, value) { + Ok(suffix) => suffix, + // A JavaScript-visible ToString failure (for example a + // Symbol operand) must surface as a completion throw after + // the canonical Add PC is published, not as an engine error. + Err(error) + if crate::engine::api::error::NativeErrorKind::from_javascript_error( + error.kind(), + ) + .is_some() => + { + return Ok(PreparedAdd::Result(Err(error))); + } + Err(error) => return Err(error), }, }; // A prepend never appends into the shared constant buffer; only an - // append may extend a uniquely-owned local in place. + // append may extend a uniquely-owned local. The handle form always + // commits a fresh node and releases the replaced local edge. + let string = super::super::numeric::string_payload(runtime, *id)?; if !prepend { - match string.try_concat_in_place(&suffix) { - Ok(true) => return Ok(PreparedAdd::Appended), - Err(error) => return Ok(PreparedAdd::Result(Err(error.into()))), - Ok(false) => {} + let mut candidate = string.clone(); + if candidate + .try_concat_in_place(&suffix) + .map_err(Error::from)? + { + let value = super::super::numeric::allocate_string_jsvalue(runtime, candidate)?; + let old = std::mem::replace(left, value); + runtime + .release_jsvalue(old) + .map_err(runtime_error_to_vm_error)?; + return Ok(PreparedAdd::Appended); } } - // Reuse the conversion already completed above even when a - // shared/rope lhs cannot append into its own buffer. return Ok(PreparedAdd::Result( - string - .try_concat(&suffix) - .map(Value::String) - .map_err(Error::from), + super::super::numeric::allocate_string_jsvalue( + runtime, + string.try_concat(&suffix).map_err(Error::from)?, + ), )); } Ok(PreparedAdd::Result( - super::super::numeric::add_primitives_ref(left, right), + super::super::numeric::add_primitives_ref(runtime, left, right), )) }; + let mut constant_operand = None; let prepared = match operands { Operands::Locals(left, right) => transaction.with_local_add_inputs(left, right, consume)?, Operands::LocalConstant(right) => { - transaction.with_local_add_constant(store, &right, consume)? + let prepared = transaction.with_local_add_constant(store, &right, consume)?; + constant_operand = Some(right); + prepared } - Operands::ConstantLocal(constant) => { - transaction.with_local_add_constant_left(store, constant, consume)? + Operands::ConstantLocal(mut constant) => { + let prepared = + transaction.with_local_add_constant_left(store, &mut constant, consume)?; + constant_operand = Some(constant); + prepared } }; + if let Some(constant) = constant_operand { + runtime + .release_jsvalue(constant) + .map_err(runtime_error_to_vm_error)?; + } let Some(prepared) = prepared else { return Ok(PrimitiveCompletion::Declined); }; @@ -150,7 +186,7 @@ pub(in crate::engine::vm) fn complete_local_add( } return Ok(PrimitiveCompletion::Throw( runtime - .new_native_error_from_error(frame.executable.realm, kind, &error) + .new_native_error_from_error_jsvalue(frame.executable.realm, kind, &error) .map_err(runtime_error_to_vm_error)?, )); } @@ -180,7 +216,7 @@ pub(in crate::engine::vm) fn complete_local_add( return Err(error); } }; - drop(old); + super::super::bindings::release_frame_binding(runtime, old)?; } (frame.fault_pc, frame.resume_pc) = (start + span - 1, start + span); #[cfg(feature = "profiling")] @@ -203,13 +239,16 @@ pub(in crate::engine::vm) fn complete_local_add( /// Extract the canonical String operand. A non-String constant declines the /// fused span; the canonical PushConst/GetLocal sequence then runs unchanged. fn constant_string( + runtime: &Runtime, executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, -) -> Option { +) -> Option { use crate::engine::heap::{BytecodeConstant, RawValue}; match executable.constant(index) { Some(BytecodeConstant::Value(RawValue::String(value))) => { - Some(Value::String(value.clone())) + // The published bytecode node owns the constant-pool edge; duplicate + // the handle so the operand carries its own independent owner. + runtime.dup_jsvalue(&JsValue::String(*value)).ok() } _ => None, } @@ -218,6 +257,7 @@ fn constant_string( #[cfg(test)] mod tests { use crate::engine::api::{Runtime, Value}; + use crate::engine::value::JsValue; #[cfg(feature = "profiling")] #[test] fn local_string_append_reaches_unique_storage_and_preserves_failure_binding() { @@ -334,8 +374,8 @@ mod tests { else { panic!("bytecode") }; - let a = context.eval("'a'").unwrap(); - let b = context.eval("'b'").unwrap(); + let a = runtime.unroot_value(&context.eval("'a'").unwrap()).unwrap(); + let b = runtime.unroot_value(&context.eval("'b'").unwrap()).unwrap(); let mut execution = RunningExecution::new( &runtime, ExecutionLimits { @@ -346,8 +386,8 @@ mod tests { .unwrap(); let entry = BytecodeCallRequest { callable, - receiver: Value::Undefined, - new_target: Value::Undefined, + receiver: JsValue::Undefined, + new_target: JsValue::Undefined, arguments: vec![], bytecode, closure_slots, @@ -377,11 +417,19 @@ mod tests { }; execution .slots - .replace_local(&frame.window, *left, FrameBinding::Direct(a.clone())) + .replace_local( + &frame.window, + *left, + FrameBinding::Direct(runtime.dup_jsvalue(&a).unwrap()), + ) .unwrap(); execution .slots - .replace_local(&frame.window, *right, FrameBinding::Direct(b.clone())) + .replace_local( + &frame.window, + *right, + FrameBinding::Direct(runtime.dup_jsvalue(&b).unwrap()), + ) .unwrap(); frame.fault_pc = start; frame.resume_pc = start; diff --git a/src/engine/vm/driver.rs b/src/engine/vm/driver.rs index 6d0b1613..f6a55bd0 100644 --- a/src/engine/vm/driver.rs +++ b/src/engine/vm/driver.rs @@ -7,8 +7,8 @@ mod ready; use crate::engine::api::error::Error; use crate::engine::api::runtime::Runtime; use crate::engine::code::function::metadata::FunctionKind; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; #[cfg(all(test, feature = "profiling"))] use crate::engine::vm::BytecodePc; use crate::engine::vm::Completion; @@ -28,8 +28,10 @@ pub(super) fn push_frame( .call_storage .reserve_depth(execution.frames.depth() + 1)?; let prepared = execution.frames.prepare_push()?; + let runtime = entry.cold.function.runtime(); let window = if entry.initialize_bindings { execution.slots.push_initialized_frame( + runtime, &entry.executable.frame_layout(), entry.storage, &entry.cold.function, @@ -38,7 +40,7 @@ pub(super) fn push_frame( } else { execution .slots - .push_frame(&entry.executable.frame_layout(), entry.storage)? + .push_frame(runtime, &entry.executable.frame_layout(), entry.storage)? }; let mut cold = entry.cold; cold.executable = entry.executable.into(); @@ -75,7 +77,9 @@ fn push_direct_call_frame( .fault_pc .checked_add(1) .ok_or_else(|| Error::internal("call resume PC overflow"))?; + let runtime = frame.cold.function.runtime().clone(); let window = execution.slots.push_call_frame( + &runtime, &entry.executable.frame_layout(), &mut frame.window, count, @@ -139,7 +143,7 @@ pub(super) fn rejected_call( }; Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -159,33 +163,35 @@ pub(super) fn enter_call( let window = &mut frame.cold.window; let count = usize::from(count); execution.slots.peek(window, count + usize::from(method))?; - let mut callable = - match runtime.direct_call_target_from_value(execution.slots.peek(window, count)?.clone()) { - Ok(super::call::DirectCallTarget::Callable(callable)) => callable, - Ok(super::call::DirectCallTarget::NonCallableProxy(proxy)) => { - // Pinned direct calls observe a non-callable Proxy's apply getter - // before reporting its missing [[Call]] capability. - let depth = execution.slots.depth(window); - let mut arguments = Vec::new(); - arguments - .try_reserve_exact(count) - .map_err(|_| Error::internal("Proxy call arguments allocation failed"))?; - for _ in 0..count { - arguments.push(execution.slots.pop(window)?); - } - arguments.reverse(); - execution.slots.pop(window)?; - let receiver = if method { - execution.slots.pop(window)? - } else { - Value::Undefined - }; - return super::proxy_get_driver::start_call( - runtime, execution, id, proxy, receiver, arguments, tail, depth, - ); + let callee = runtime + .dup_jsvalue(execution.slots.peek(window, count)?) + .map_err(runtime_error_to_vm_error)?; + let mut callable = match runtime.direct_call_target_from_jsvalue(callee) { + Ok(super::call::DirectCallTarget::Callable(callable)) => callable, + Ok(super::call::DirectCallTarget::NonCallableProxy(proxy)) => { + // Pinned direct calls observe a non-callable Proxy's apply getter + // before reporting its missing [[Call]] capability. + let depth = execution.slots.depth(window); + let mut arguments = Vec::new(); + arguments + .try_reserve_exact(count) + .map_err(|_| Error::internal("Proxy call arguments allocation failed"))?; + for _ in 0..count { + arguments.push(execution.slots.pop(window)?); } - Err(error) => return rejected_call(runtime, realm, runtime_error_to_vm_error(error)), - }; + arguments.reverse(); + execution.slots.pop(window)?; + let receiver = if method { + execution.slots.pop(window)? + } else { + JsValue::Undefined + }; + return super::proxy_get_driver::start_call( + runtime, execution, id, proxy, receiver, arguments, tail, depth, + ); + } + Err(error) => return rejected_call(runtime, realm, runtime_error_to_vm_error(error)), + }; // Keep the existing rejection order and exception materialization until // general call errors join the owned unwind path. Nothing was consumed. if !execution @@ -194,8 +200,8 @@ pub(super) fn enter_call( { return Ok(CallStep::Bridge); } - let mut bound_arguments = None; - let mut bound_receiver = None; + let mut bound_arguments: Option> = None; + let mut bound_receiver: Option = None; let (bytecode, closure_slots) = loop { if let Some(mut selected) = super::frames::NativeClassification::select(runtime, &callable) .map_err(runtime_error_to_vm_error)? @@ -210,7 +216,7 @@ pub(super) fn enter_call( )?; let (arguments, receiver) = execution .slots - .take_native_call_operands(window, count, method)?; + .take_native_call_operands(runtime, window, count, method)?; return super::proxy_get_driver::start_native_with_classification( runtime, execution, @@ -219,8 +225,20 @@ pub(super) fn enter_call( target, defining_realm, min_readable_args, - bound_receiver.unwrap_or(receiver), - bound_arguments.unwrap_or(arguments), + match bound_receiver { + Some(bound) => runtime + .root_and_release_jsvalue(bound) + .map_err(runtime_error_to_vm_error)?, + None => receiver, + }, + match bound_arguments { + Some(bound) => bound + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, + None => arguments, + }, tail, depth, Some(selected), @@ -241,9 +259,9 @@ pub(super) fn enter_call( CallableExecution::Bound { target, this_value, - arguments, + arguments: bound, } => { - let call_arguments = match bound_arguments.take() { + let accumulated = match bound_arguments.take() { Some(arguments) => arguments, None => { let mut arguments = Vec::new(); @@ -251,18 +269,27 @@ pub(super) fn enter_call( .try_reserve_exact(count) .map_err(|_| Error::internal("call arguments allocation failed"))?; for offset in (0..count).rev() { - arguments.push(execution.slots.peek(window, offset)?.clone()); + arguments.push( + runtime + .dup_jsvalue(execution.slots.peek(window, offset)?) + .map_err(runtime_error_to_vm_error)?, + ); } arguments } }; + // The helper transfers the bound payload roots into internal + // values without a retain/release pair. bound_arguments = Some( match runtime - .concatenate_bound_arguments(realm, &arguments, &call_arguments) + .concatenate_bound_arguments_jsvalue(realm, bound, accumulated) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(arguments) => arguments, NativeConversion::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(CallStep::Complete(Completion::Throw(value))); } }, @@ -284,7 +311,7 @@ pub(super) fn enter_call( let receiver = if method { execution.slots.pop(window)? } else { - Value::Undefined + JsValue::Undefined }; return super::proxy_get_driver::start_call( runtime, @@ -311,7 +338,7 @@ pub(super) fn enter_call( )?; let (arguments, receiver) = execution .slots - .take_native_call_operands(window, count, method)?; + .take_native_call_operands(runtime, window, count, method)?; return super::proxy_get_driver::start_classified_native_call( runtime, execution, @@ -320,8 +347,20 @@ pub(super) fn enter_call( target, defining_realm, min_readable_args, - bound_receiver.unwrap_or(receiver), - bound_arguments.unwrap_or(arguments), + match bound_receiver { + Some(bound) => runtime + .root_and_release_jsvalue(bound) + .map_err(runtime_error_to_vm_error)?, + None => receiver, + }, + match bound_arguments { + Some(bound) => bound + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, + None => arguments, + }, tail, depth, ); @@ -333,14 +372,29 @@ pub(super) fn enter_call( )?; let (arguments, receiver) = execution .slots - .take_native_call_operands(window, count, method)?; + .take_native_call_operands(runtime, window, count, method)?; + // The rooted native argv crosses back into the internal call + // convention: its edges are duplicated and the public roots + // release through their own Drop path. return super::proxy_get_driver::start_callback_call( runtime, execution, id, callable, - bound_receiver.unwrap_or(receiver), - bound_arguments.unwrap_or(arguments), + match bound_receiver { + Some(bound) => bound, + None => runtime + .unroot_value(&receiver) + .map_err(runtime_error_to_vm_error)?, + }, + match bound_arguments { + Some(bound) => bound, + None => arguments + .iter() + .map(|argument| runtime.unroot_value(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, + }, tail, depth, ); @@ -367,14 +421,14 @@ pub(super) fn enter_call( if kind == FunctionKind::Normal && bound_arguments.is_none() { let frame = execution.frames.current_mut(id)?; let receiver = if method { - super::stack::copy_value(execution.slots.peek(&frame.window, count + 1)?)? + super::stack::copy_value(runtime, execution.slots.peek(&frame.window, count + 1)?)? } else { - Value::Undefined + JsValue::Undefined }; let request = BytecodeCallRequest { callable, receiver, - new_target: Value::Undefined, + new_target: JsValue::Undefined, arguments: Vec::new(), bytecode, closure_slots, @@ -402,11 +456,20 @@ pub(super) fn enter_call( let receiver = if method { execution.slots.pop(&mut frame.window)? } else { - Value::Undefined + JsValue::Undefined }; // The checked callable root now owns the popped callee identity. - drop(function); + runtime + .release_jsvalue(function) + .map_err(runtime_error_to_vm_error)?; if let Some(normalized) = bound_arguments { + // The superseded operand edges surrender before the normalized owners + // move into the request. + for argument in arguments { + runtime + .release_jsvalue(argument) + .map_err(runtime_error_to_vm_error)?; + } arguments = normalized; } let receiver = bound_receiver.unwrap_or(receiver); @@ -419,7 +482,7 @@ pub(super) fn enter_call( let request = BytecodeCallRequest { callable, receiver, - new_target: Value::Undefined, + new_target: JsValue::Undefined, arguments, bytecode, closure_slots, @@ -493,18 +556,28 @@ pub(crate) fn execute_root( realm: crate::engine::heap::ContextId, operation: RootOperation, ) -> Result { - start_root(runtime.clone(), realm, operation)?.finish(runtime) + start_root(&runtime, realm, operation)?.finish(runtime) } pub(super) fn execute_root_descriptor( runtime: Runtime, realm: crate::engine::heap::ContextId, operation: RootOperation, ) -> Result { - match start_root(runtime, realm, operation)? { + match start_root(&runtime, realm, operation)? { RunningExit::RootDescriptor(result) => Ok(result), - RunningExit::Complete(Completion::Throw(value)) => Ok( - crate::engine::value::conversion::NativeConversion::Throw(value), - ), + RunningExit::Complete(Completion::Throw(value)) => { + // The internal exception crosses out to the public host adapter: + // its root is duplicated and the internal edge is released. + let rooted = runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + Ok(crate::engine::value::conversion::NativeConversion::Throw( + rooted, + )) + } _ => Err(Error::internal( "descriptor entry returned an untyped terminal result", )), @@ -512,17 +585,17 @@ pub(super) fn execute_root_descriptor( } fn start_root( - runtime: Runtime, + runtime: &Runtime, realm: crate::engine::heap::ContextId, operation: RootOperation, ) -> Result { - let mut execution = RunningExecution::new(&runtime, ExecutionLimits::for_runtime(&runtime))?; - match super::proxy_get_driver::start_root(&runtime, &mut execution, realm, operation)? { + let mut execution = RunningExecution::new(runtime, ExecutionLimits::for_runtime(runtime))?; + match super::proxy_get_driver::start_root(runtime, &mut execution, realm, operation)? { super::proxy_get_driver::Progress::Call(CallStep::Complete(completion)) => { Ok(RunningExit::Complete(completion)) } super::proxy_get_driver::Progress::Call(CallStep::Entered) => { - run_frames(&runtime, execution) + run_frames(runtime, execution) } _ => Err(Error::internal( "root operation returned a bytecode-only continuation", @@ -611,7 +684,15 @@ fn run_frames_with_state( .cold .resume_throw .take() - .map(Completion::Throw); + .map(|value| { + // The rare-cell throw is a public-root island; entering the + // internal completion duplicates its edges at this boundary. + runtime + .unroot_value(&value) + .map(Completion::Throw) + .map_err(runtime_error_to_vm_error) + }) + .transpose()?; } let mut conversion_prepared = false; let mut exit = if let Some(task) = conversion.take() { diff --git a/src/engine/vm/driver/cold.rs b/src/engine/vm/driver/cold.rs index c40958ca..9aa997bf 100644 --- a/src/engine/vm/driver/cold.rs +++ b/src/engine/vm/driver/cold.rs @@ -462,36 +462,24 @@ fn convert( let execution = &mut *context.execution; let id = context.id; - let mut invalid = false; if !context.conversion_prepared { let frame = execution.frames.current_mut(id)?; for offset in (0..=usize::from(addition)).rev() { - invalid |= runtime - .validate_value_domain( - execution.slots.peek(&frame.window, offset)?, - "conversion operand", - ) - .is_err(); + execution.slots.peek(&frame.window, offset)?; } + (*context.next_operation) = (*context.next_operation) + .checked_add(1) + .ok_or_else(|| Error::internal("conversion identity exhausted"))?; } - if invalid { - Ok(Disposition::Bridge) - } else { - if !context.conversion_prepared { - (*context.next_operation) = (*context.next_operation) - .checked_add(1) - .ok_or_else(|| Error::internal("conversion identity exhausted"))?; - } - *context.conversion = Some(crate::engine::vm::conversion_driver::ConversionTask::start( - runtime, - execution, - id, - *context.next_operation, - addition, - property_key, - )?); - Ok(Disposition::Entered) - } + *context.conversion = Some(crate::engine::vm::conversion_driver::ConversionTask::start( + runtime, + execution, + id, + *context.next_operation, + addition, + property_key, + )?); + Ok(Disposition::Entered) } #[inline(never)] @@ -614,7 +602,7 @@ fn set_property(context: &mut Context<'_>, key: Option) -> Result>; pub(crate) fn call( @@ -46,16 +46,24 @@ pub(crate) fn construct( let (constructor, new_target) = match runtime.prepare_constructor_pair(realm, constructor, new_target)? { NativeConversion::Value(pair) => pair, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(runtime.into_jsvalue(value)?)); + } }; + let mut js_arguments = Vec::with_capacity(arguments.len()); + for argument in arguments { + js_arguments.push(runtime.unroot_value(argument)?); + } let normalized = match runtime.normalize_constructor( realm, constructor, ConstructNewTarget::Validated(new_target), - arguments.to_vec(), + js_arguments, )? { NativeConversion::Value(normalized) => normalized, - NativeConversion::Throw(value) => return Ok(Completion::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(Completion::Throw(runtime.into_jsvalue(value)?)); + } }; execute_root(runtime.clone(), realm, RootOperation::Construct(normalized)) .map_err(RuntimeError::Engine) @@ -109,6 +117,7 @@ pub(crate) fn define( runtime.validate_object_and_key(object, key)?; runtime.validate_descriptor_domains(descriptor)?; boolean( + runtime, execute_root( runtime.clone(), realm, @@ -133,6 +142,7 @@ pub(crate) fn set( runtime.validate_value_domain(&value, "property value")?; runtime.validate_value_domain(&receiver, "property receiver")?; boolean( + runtime, execute_root( runtime.clone(), realm, @@ -146,10 +156,15 @@ pub(crate) fn set( .map_err(RuntimeError::Engine)?, ) } -fn boolean(completion: Completion) -> Result, RuntimeError> { +fn boolean( + runtime: &Runtime, + completion: Completion, +) -> Result, RuntimeError> { match completion { - Completion::Return(Value::Bool(value)) => Ok(NativeConversion::Value(value)), - Completion::Throw(value) => Ok(NativeConversion::Throw(value)), + Completion::Return(JsValue::Bool(value)) => Ok(NativeConversion::Value(value)), + Completion::Throw(value) => Ok(NativeConversion::Throw( + runtime.root_and_release_jsvalue(value)?, + )), _ => Err(RuntimeError::Invariant( "property entry did not return a boolean", )), diff --git a/src/engine/vm/environment_bindings.rs b/src/engine/vm/environment_bindings.rs index a2eecd0d..4b3abcc1 100644 --- a/src/engine/vm/environment_bindings.rs +++ b/src/engine/vm/environment_bindings.rs @@ -96,6 +96,9 @@ pub(super) fn eval_variable_object<'a>( .map_err(runtime_error_to_vm_error)? } }; + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; let Value::Object(object) = value else { return Err(Error::internal( "eval variable-object binding did not contain an Object", @@ -191,6 +194,9 @@ pub(super) fn with_object<'a>( .map_err(runtime_error_to_vm_error)? } }; + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; let Value::Object(object) = value else { return Err(Error::internal( "with-object binding did not contain an Object", diff --git a/src/engine/vm/environment_bindings/operation.rs b/src/engine/vm/environment_bindings/operation.rs index da8163db..fd56380f 100644 --- a/src/engine/vm/environment_bindings/operation.rs +++ b/src/engine/vm/environment_bindings/operation.rs @@ -3,7 +3,7 @@ use crate::engine::{ api::{ErrorKind, runtime::Runtime, runtime_error::RuntimeError}, heap::ContextId, object::{ObjectRef, PropertyKey, WellKnownSymbol, operations::InternalSetResult}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion}, vm::Completion, }; @@ -50,7 +50,7 @@ enum Phase { Put { object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, strict: bool, reference: bool, }, @@ -70,12 +70,14 @@ enum Phase { } impl EnvironmentStep { pub(in crate::engine::vm) fn read( + runtime: &Runtime, realm: ContextId, object: ObjectRef, key: PropertyKey, - receiver: Value, + receiver: JsValue, ) -> Self { Self::request_read( + runtime, receiver, object, key, @@ -126,7 +128,7 @@ impl EnvironmentStep { realm: ContextId, object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, strict: bool, reference: bool, ) -> Self { @@ -150,7 +152,7 @@ impl EnvironmentStep { realm: ContextId, object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, strict: bool, ) -> Self { Self::request_set( @@ -219,19 +221,26 @@ impl EnvironmentResume { let present = match reply { NativeConversion::Value(value) => value, NativeConversion::Throw(value) => { - return Ok(EnvironmentStep::Complete(Completion::Throw(value))); + return Ok(EnvironmentStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let realm = self.0.realm; match self.0.phase { Phase::Binding { object, key, with } => { if !present || !with { - return Ok(EnvironmentStep::Complete(Completion::Return(Value::Bool( - present, - )))); + return Ok(EnvironmentStep::Complete(Completion::Return( + JsValue::Bool(present), + ))); } Ok(EnvironmentStep::request_read( - Value::Object(object.clone()), + runtime, + { + let id = object.object_id(); + runtime.retain_object_handle(id)?; + JsValue::Object(id) + }, object, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Unscopables)), EnvironmentResume(Box::new(EnvironmentResumeState { @@ -253,11 +262,16 @@ impl EnvironmentResume { .into()); } return Ok(EnvironmentStep::Complete(Completion::Return( - Value::Undefined, + JsValue::Undefined, ))); } Ok(EnvironmentStep::request_read( - Value::Object(object.clone()), + runtime, + { + let id = object.object_id(); + runtime.retain_object_handle(id)?; + JsValue::Object(id) + }, object, key, EnvironmentResume(Box::new(EnvironmentResumeState { @@ -300,26 +314,26 @@ impl EnvironmentResume { runtime.write_var_ref(&root, value)?; } return Ok(EnvironmentStep::Complete(Completion::Return( - Value::Undefined, + JsValue::Undefined, ))); } Ok(EnvironmentStep::set(realm, object, key, value, strict)) } Phase::Reference { object } => { Ok(EnvironmentStep::Complete(Completion::Return(if present { - Value::Object(object) + JsValue::Object(object.object_id()) } else { - Value::Undefined + JsValue::Undefined }))) } Phase::DeleteGlobal { object, key } => Ok(if present { EnvironmentStep::delete(realm, object, key) } else { - EnvironmentStep::Complete(Completion::Return(Value::Bool(true))) + EnvironmentStep::Complete(Completion::Return(JsValue::Bool(true))) }), - Phase::Boolean => Ok(EnvironmentStep::Complete(Completion::Return(Value::Bool( - present, - )))), + Phase::Boolean => Ok(EnvironmentStep::Complete(Completion::Return( + JsValue::Bool(present), + ))), _ => Err(RuntimeError::Invariant( "environment Boolean reply has wrong phase", )), @@ -338,9 +352,10 @@ impl EnvironmentResume { }; match self.0.phase { Phase::Unscopables { key } => Ok(match value { - Value::Object(object) => EnvironmentStep::request_read( - Value::Object(object.clone()), - object, + JsValue::Object(object) => EnvironmentStep::request_read( + runtime, + JsValue::Object(object), + ObjectRef::from_borrowed_handle(runtime.clone(), object)?, key, EnvironmentResume(Box::new(EnvironmentResumeState { pending_effect: EnvironmentStepPending::default(), @@ -348,11 +363,11 @@ impl EnvironmentResume { phase: Phase::Excluded, })), ), - _ => EnvironmentStep::Complete(Completion::Return(Value::Bool(true))), + _ => EnvironmentStep::Complete(Completion::Return(JsValue::Bool(true))), }), - Phase::Excluded => Ok(EnvironmentStep::Complete(Completion::Return(Value::Bool( - !runtime.value_to_boolean(&value)?, - )))), + Phase::Excluded => Ok(EnvironmentStep::Complete(Completion::Return( + JsValue::Bool(!runtime.value_to_boolean_jsvalue(&value)?), + ))), Phase::Value => Ok(EnvironmentStep::Complete(Completion::Return(value))), _ => Err(RuntimeError::Invariant( "environment value reply has wrong phase", @@ -381,10 +396,10 @@ struct EnvironmentStepPending { has_key: Option, read_object: Option, read_key: Option, - read_receiver: Option, + read_receiver: Option, set_object: Option, set_key: Option, - set_value: Option, + set_value: Option, delete_object: Option, delete_key: Option, } @@ -399,11 +414,14 @@ impl EnvironmentStep { Self::Has { resume } } pub(crate) fn request_read( - receiver: Value, + _runtime: &Runtime, + receiver: JsValue, object: ObjectRef, key: PropertyKey, mut resume: EnvironmentResume, ) -> Self { + // The receiver carries an owned edge: either the caller retained it + // above, or the completion value already owned its edge. resume.0.pending_effect.read_object = Some(object); resume.0.pending_effect.read_key = Some(key); resume.0.pending_effect.read_receiver = Some(receiver); @@ -412,7 +430,7 @@ impl EnvironmentStep { pub(crate) fn request_set( object: ObjectRef, key: PropertyKey, - value: Value, + value: JsValue, mut resume: EnvironmentResume, ) -> Self { resume.0.pending_effect.set_object = Some(object); @@ -459,7 +477,7 @@ impl EnvironmentResume { .take() .expect("EnvironmentStep Read key") } - pub(crate) fn take_read_receiver(&mut self) -> Value { + pub(crate) fn take_read_receiver(&mut self) -> JsValue { self.0 .pending_effect .read_receiver @@ -480,7 +498,7 @@ impl EnvironmentResume { .take() .expect("EnvironmentStep Set key") } - pub(crate) fn take_set_value(&mut self) -> Value { + pub(crate) fn take_set_value(&mut self) -> JsValue { self.0 .pending_effect .set_value diff --git a/src/engine/vm/environment_driver.rs b/src/engine/vm/environment_driver.rs index de878393..2e3dcb42 100644 --- a/src/engine/vm/environment_driver.rs +++ b/src/engine/vm/environment_driver.rs @@ -3,11 +3,14 @@ use super::driver::CallStep; use super::environment_bindings::operation::EnvironmentStep; use super::frame::ReturnValue; use super::{ - Completion, exception::runtime_error_to_vm_error, execution::RunningExecution, frame::FrameId, + Completion, + exception::{heap_error_to_vm_error, runtime_error_to_vm_error}, + execution::RunningExecution, + frame::FrameId, }; use crate::engine::api::{error::Error, runtime::Runtime}; use crate::engine::code::bytecode::{DynamicEnvironmentSource, EvalVariableSource}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum WriteTarget { @@ -70,7 +73,7 @@ pub(super) fn try_global_own_read( executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, roots: &super::closure::ClosureSlots, index: u16, -) -> Result, Error> { +) -> Result, Error> { use crate::engine::code::function::metadata::ClosureVariableName; let Some(descriptor) = executable.closure_variables.get(usize::from(index)) else { return Ok(None); @@ -132,7 +135,7 @@ pub(super) fn step( } else { execution .slots - .push(&mut frame.window, Value::Bool(false))?; + .push(&mut frame.window, JsValue::Bool(false))?; } } Operation::GlobalReference(index) => { @@ -144,9 +147,15 @@ pub(super) fn step( &frame.cold.closure_slots, index, )? { - GlobalReference::Lexical(object) => execution - .slots - .push(&mut frame.window, Value::Object(object))?, + GlobalReference::Lexical(object) => { + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + execution + .slots + .push(&mut frame.window, JsValue::Object(id))? + } GlobalReference::Object { object, key } => { query = Some(( EnvironmentStep::reference(realm, object, key), @@ -194,8 +203,14 @@ pub(super) fn step( &frame.cold.closure_slots, )?, WriteTarget::Reference => match execution.slots.peek(&frame.window, 1)? { - Value::Object(object) if object.belongs_to(runtime) => object.clone(), - Value::Undefined if strict => { + JsValue::Object(object) => { + crate::engine::object::ObjectRef::from_borrowed_handle( + runtime.clone(), + *object, + ) + .map_err(heap_error_to_vm_error)? + } + JsValue::Undefined if strict => { return Err(runtime .native_atom_error( crate::engine::api::ErrorKind::Reference, @@ -205,7 +220,7 @@ pub(super) fn step( ) .map_err(runtime_error_to_vm_error)?); } - Value::Undefined => runtime + JsValue::Undefined => runtime .global_object_for_realm(realm) .map_err(runtime_error_to_vm_error)?, _ => return Err(Error::internal("invalid dynamic reference base")), @@ -257,21 +272,27 @@ pub(super) fn step( } let key = linked_key(runtime, &frame.executable, name)?; let value = execution.slots.pop(&mut frame.window)?; - match runtime + let value_root = runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?; + let defined = runtime .define_own_property_in_realm( Some(realm), &object, &key, &OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), + value: DescriptorField::Present(value_root), writable: DescriptorField::Present(true), enumerable: DescriptorField::Present(true), configurable: DescriptorField::Present(true), ..OrdinaryPropertyDescriptor::new() }, ) - .map_err(runtime_error_to_vm_error)? - { + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + match defined { PropertyDefineOutcome::Defined(true) => {} PropertyDefineOutcome::Defined(false) => { return Err(Error::new( @@ -280,6 +301,9 @@ pub(super) fn step( )); } PropertyDefineOutcome::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(CallStep::Complete(Completion::Throw(value))); } } @@ -317,9 +341,15 @@ pub(super) fn step( } else { let object = match op { Operation::ReadReference { name, .. } => { - let object = match execution.slots.peek(&frame.window, 0)? { - Value::Object(object) => object, - Value::Undefined => { + match execution.slots.peek(&frame.window, 0)? { + JsValue::Object(object) => { + crate::engine::object::ObjectRef::from_borrowed_handle( + runtime.clone(), + *object, + ) + .map_err(heap_error_to_vm_error)? + } + JsValue::Undefined => { let key = linked_key(runtime, &frame.executable, name)?; return Err(runtime .native_atom_error( @@ -331,13 +361,7 @@ pub(super) fn step( .map_err(runtime_error_to_vm_error)?); } _ => return Err(Error::internal("invalid dynamic reference base")), - }; - if !object.belongs_to(runtime) { - return Err(Error::internal( - "dynamic reference base belongs to another runtime", - )); } - object.clone() } Operation::Object(source) | Operation::Get { source, .. } => { super::environment_bindings::dynamic_object( @@ -351,7 +375,11 @@ pub(super) fn step( _ => unreachable!(), }; if matches!(op, Operation::Object(_)) { - BindingRead::Value(Value::Object(object)) + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + BindingRead::Value(JsValue::Object(id)) } else { read_binding(runtime, &frame.executable, &object, op)? } @@ -374,19 +402,27 @@ pub(super) fn step( } values.reverse(); let array = runtime - .new_array_from_values(realm, values) + .new_array_from_values_jsvalue(realm, values) .map_err(runtime_error_to_vm_error)?; + let id = array.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; execution .slots - .push(&mut frame.window, Value::Object(array))?; + .push(&mut frame.window, JsValue::Object(id))?; } Operation::CreateObject => { let object = runtime .new_ordinary_object_in_realm(realm) .map_err(runtime_error_to_vm_error)?; + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; execution .slots - .push(&mut frame.window, Value::Object(object))?; + .push(&mut frame.window, JsValue::Object(id))?; } Operation::CreateVariable => { if frame @@ -403,20 +439,33 @@ pub(super) fn step( let object = runtime .new_object(None) .map_err(runtime_error_to_vm_error)?; + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; execution .slots - .push(&mut frame.window, Value::Object(object))?; + .push(&mut frame.window, JsValue::Object(id))?; } Operation::ToObject => { let value = execution.slots.pop(&mut frame.window)?; match runtime - .native_to_object(realm, value) + .native_to_object_jsvalue(realm, value) .map_err(runtime_error_to_vm_error)? { - NativeConversion::Value(object) => execution - .slots - .push(&mut frame.window, Value::Object(object))?, + NativeConversion::Value(object) => { + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + execution + .slots + .push(&mut frame.window, JsValue::Object(id))? + } NativeConversion::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(CallStep::Complete(Completion::Throw(value))); } } @@ -470,7 +519,7 @@ pub(super) fn step( }; Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -478,7 +527,7 @@ pub(super) fn step( } enum BindingRead { - Value(Value), + Value(JsValue), Getter { getter: crate::engine::object::CallableRef, receiver: Value, @@ -533,7 +582,13 @@ fn read_binding( "lexical reference lost its data descriptor", )); }; - return Ok(BindingRead::Value(value)); + // The descriptor root transfers into the internal value without a + // retain/release pair. + return Ok(BindingRead::Value( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + )); } } Ok(BindingRead::Query(EnvironmentStep::get( @@ -598,13 +653,18 @@ pub(super) fn prepare_environment_read( .map_err(runtime_error_to_vm_error)? { Some(CompleteOrdinaryPropertyDescriptor::Data { value, .. }) => { - Ok(OrdinaryRead::Complete(Some(value))) + Ok(OrdinaryRead::Complete(Some( + // The descriptor root transfers without a retain/release pair. + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))) } Some(CompleteOrdinaryPropertyDescriptor::Accessor { get: Some(getter), .. }) => Ok(OrdinaryRead::Call { getter, receiver }), Some(CompleteOrdinaryPropertyDescriptor::Accessor { get: None, .. }) => { - Ok(OrdinaryRead::Complete(Some(Value::Undefined))) + Ok(OrdinaryRead::Complete(Some(JsValue::Undefined))) } None => match runtime .get_prototype_of(object) @@ -654,8 +714,10 @@ fn read_global_binding( .raw_var_ref_value(&root) .map_err(runtime_error_to_vm_error)?; if !matches!(value, RawValue::Uninitialized) { + let value = JsValue::from_raw(value) + .ok_or_else(|| Error::internal("global cell held an internal value sentinel"))?; return runtime - .root_raw_value(&value) + .dup_jsvalue(&value) .map(BindingRead::Value) .map_err(runtime_error_to_vm_error); } @@ -684,16 +746,24 @@ fn read_global_binding( "' is not defined", ) .map_err(runtime_error_to_vm_error)?), - OrdinaryRead::Complete(None) => Ok(BindingRead::Value(Value::Undefined)), + OrdinaryRead::Complete(None) => Ok(BindingRead::Value(JsValue::Undefined)), OrdinaryRead::Call { getter, receiver } => Ok(BindingRead::Getter { getter, receiver }), OrdinaryRead::Special { object, receiver, .. - } => Ok(BindingRead::Query(EnvironmentStep::read( - executable.realm, - object, - key, - receiver, - ))), + } => { + // The special receiver is a public root; entering the internal + // step duplicates its edge at this boundary. + let receiver = runtime + .unroot_value(&receiver) + .map_err(runtime_error_to_vm_error)?; + Ok(BindingRead::Query(EnvironmentStep::read( + runtime, + executable.realm, + object, + key, + receiver, + ))) + } } } @@ -738,7 +808,10 @@ mod tests { .unwrap(); let _costs = profile.snapshot(); assert!( - matches!(completion, Completion::Return(Value::Int(42))), + matches!( + completion, + Completion::Return(crate::engine::value::JsValue::Int(42)) + ), "{source}: {completion:?}" ); assert!(runtime.0.state.borrow().active_frames.is_empty()); diff --git a/src/engine/vm/eval_driver.rs b/src/engine/vm/eval_driver.rs index 4f2849e8..6cc22bd4 100644 --- a/src/engine/vm/eval_driver.rs +++ b/src/engine/vm/eval_driver.rs @@ -4,7 +4,7 @@ use super::{ call::{BytecodeCallRequest, CallableExecution}, driver::{CallStep, push_frame}, eval_bindings::{self, PreparedEvalEnvironment}, - exception::runtime_error_to_vm_error, + exception::{heap_error_to_vm_error, runtime_error_to_vm_error}, execution::RunningExecution, frame::{FrameId, OperationTarget, ReturnTarget, ReturnValue}, }; @@ -12,7 +12,7 @@ use crate::engine::{ api::{Error, runtime::Runtime}, builtins::DirectEvalPreparation, code::function::metadata::EvalBindingSource, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, }; #[inline(never)] @@ -29,7 +29,7 @@ pub(super) fn step( .slots .peek(&frame.window, usize::from(arguments))?; if !runtime - .is_original_eval(realm, function) + .is_original_eval_jsvalue(realm, function) .map_err(runtime_error_to_vm_error)? { return super::driver::enter_call(runtime, execution, id, arguments, false, false); @@ -46,7 +46,7 @@ pub(super) fn step( }; Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -62,43 +62,79 @@ fn prepare_and_enter( let can_push = execution.frames.can_push(); let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; + // `input` stays a public root: the direct-eval preparation consumes the + // public invocation form, and the rare-cell cache is a public-root island. let input = if let Some(values) = &frame.cold.eval_arguments { values.first().cloned().unwrap_or(Value::Undefined) } else if arguments == 0 { Value::Undefined } else { - execution - .slots - .peek(&frame.window, usize::from(arguments) - 1)? - .clone() + runtime + .root_value( + execution + .slots + .peek(&frame.window, usize::from(arguments) - 1)?, + ) + .map_err(runtime_error_to_vm_error)? }; let string = matches!(input, Value::String(_)); let this_value = if !string { - frame.cold.input.this_value.clone() + runtime + .dup_jsvalue(&frame.cold.input.this_value) + .map_err(runtime_error_to_vm_error)? } else if let Some(value) = frame .cold .rare .get() .and_then(|rare| rare.normalized_this.as_ref()) { - value.clone() + runtime + .unroot_value(value) + .map_err(runtime_error_to_vm_error)? } else if frame.executable.metadata.strict - || matches!(frame.cold.input.this_value, Value::Object(_)) + || matches!(frame.cold.input.this_value, JsValue::Object(_)) { - frame.cold.input.this_value.clone() - } else if matches!(frame.cold.input.this_value, Value::Null | Value::Undefined) { - Value::Object(frame.cold.input.callee_global(runtime, realm)?.clone()) + runtime + .dup_jsvalue(&frame.cold.input.this_value) + .map_err(runtime_error_to_vm_error)? + } else if matches!( + frame.cold.input.this_value, + JsValue::Null | JsValue::Undefined + ) { + let id = frame.cold.input.callee_global(runtime, realm)?.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + JsValue::Object(id) } else { let value = match runtime - .native_to_object(realm, frame.cold.input.this_value.clone()) + .native_to_object_jsvalue( + realm, + runtime + .dup_jsvalue(&frame.cold.input.this_value) + .map_err(runtime_error_to_vm_error)?, + ) .map_err(runtime_error_to_vm_error)? { - NativeConversion::Value(object) => Value::Object(object), + NativeConversion::Value(object) => { + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + JsValue::Object(id) + } NativeConversion::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(CallStep::Complete(Completion::Throw(value))); } }; - frame.cold.normalized_this = Some(value.clone()); + frame.cold.normalized_this = Some( + runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?, + ); value }; let prepared = if string { @@ -130,8 +166,9 @@ fn prepare_and_enter( let invocation = DirectEvalInvocation { input, environment, - this_value, - new_target: frame.cold.input.new_target.clone(), + this_value: runtime + .root_and_release_jsvalue(this_value) + .map_err(runtime_error_to_vm_error)?, caller_strict: frame.executable.metadata.strict, }; let prepared = runtime @@ -189,8 +226,10 @@ fn prepare_and_enter( } Some(BytecodeCallRequest { callable, - receiver: this_value, - new_target: Value::Undefined, + receiver: runtime + .into_jsvalue(this_value) + .map_err(runtime_error_to_vm_error)?, + new_target: JsValue::Undefined, arguments: Vec::new(), bytecode, closure_slots, @@ -227,10 +266,13 @@ pub(super) fn apply( let can_push = execution.frames.can_push(); let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; - let Value::Object(array) = execution.slots.peek(&frame.window, 0)? else { + let array = runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?; + let Value::Object(array) = array else { return Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error( + .new_native_error_jsvalue( realm, crate::engine::api::error::NativeErrorKind::Type, "not a object", @@ -239,18 +281,27 @@ pub(super) fn apply( ))); }; let Some(values) = runtime - .prepare_fast_array_arguments(realm, array) + .prepare_fast_array_arguments(realm, &array) .map_err(runtime_error_to_vm_error)? else { return Ok(CallStep::Bridge); }; + // The fast-array snapshot stays public-rooted: the eval-arguments rare + // cell is a public-root island, and the apply request re-enters the + // internal convention at its boundary below. let mut values = match values { NativeConversion::Value(values) => values, - NativeConversion::Throw(value) => return Ok(CallStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(CallStep::Complete(Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))); + } }; let function = execution.slots.peek(&frame.window, 1)?; if runtime - .is_original_eval(realm, function) + .is_original_eval_jsvalue(realm, function) .map_err(runtime_error_to_vm_error)? { if frame.cold.eval_arguments.is_some() { @@ -259,11 +310,11 @@ pub(super) fn apply( frame.cold.eval_arguments = Some(values); return step(runtime, execution, id, 1, environment); } - let Value::Object(function) = function else { + let JsValue::Object(function) = function else { return Ok(CallStep::Bridge); }; let Some(mut callable) = runtime - .as_callable(function) + .as_callable_object(*function) .map_err(runtime_error_to_vm_error)? else { return Ok(CallStep::Bridge); @@ -283,17 +334,28 @@ pub(super) fn apply( this_value, arguments, } => { + let bound = arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; values = match runtime - .concatenate_bound_arguments(realm, &arguments, &values) + .concatenate_bound_arguments(realm, &bound, &values) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(values) => values, NativeConversion::Throw(value) => { - return Ok(CallStep::Complete(Completion::Throw(value))); + return Ok(CallStep::Complete(Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))); } }; callable = target; - receiver = this_value; + receiver = runtime + .root_and_release_jsvalue(this_value) + .map_err(runtime_error_to_vm_error)?; } _ => return Ok(CallStep::Bridge), } @@ -319,9 +381,15 @@ pub(super) fn apply( } let request = BytecodeCallRequest { callable, - receiver, - new_target: Value::Undefined, - arguments: values, + receiver: runtime + .unroot_value(&receiver) + .map_err(runtime_error_to_vm_error)?, + new_target: JsValue::Undefined, + arguments: values + .iter() + .map(|value| runtime.unroot_value(value)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, bytecode, closure_slots, caller_realm: realm, @@ -495,7 +563,7 @@ mod capture_tests { .unwrap(); let child = runtime.test_child_function_bytecode(&parent, 0).unwrap(); let closure = runtime - .new_var_ref(Value::Int(30), false, false, ClosureVariableKind::Normal) + .new_var_ref(JsValue::Int(30), false, false, ClosureVariableKind::Normal) .unwrap(); let eval_variable_object = runtime.new_object(None).unwrap(); @@ -544,13 +612,17 @@ mod capture_tests { input: prepared.input.into(), }), storage: FrameStorage { - original_arguments: vec![Value::Int(10)], + original_arguments: vec![JsValue::Int(10)], parameters: prepared.arguments, locals: vec![ - FrameBinding::Direct(Value::Int(20)), - FrameBinding::Direct(Value::Object(eval_variable_object.clone())), + FrameBinding::Direct(JsValue::Int(20)), + FrameBinding::Direct( + runtime + .into_jsvalue(Value::Object(eval_variable_object.clone())) + .unwrap(), + ), ], - operands: vec![Value::Undefined], + operands: vec![JsValue::Undefined], }, }; let mut execution = @@ -577,7 +649,7 @@ mod capture_tests { &mut execution, child_id, super::super::run::RunExit::Complete, - Some(Completion::Return(Value::Undefined)), + Some(Completion::Return(JsValue::Undefined)), ) .unwrap(); } @@ -598,11 +670,11 @@ mod capture_tests { ), captured ); - assert_eq!(runtime.read_var_ref(&closure).unwrap(), Value::Int(30)); + assert_eq!(runtime.read_var_ref(&closure).unwrap(), JsValue::Int(30)); if !captured && !throws { assert_eq!( execution.slots.peek(&frame.window, 0).unwrap(), - &Value::Int(42) + &JsValue::Int(42) ); } } diff --git a/src/engine/vm/exception.rs b/src/engine/vm/exception.rs index fce2177c..576c7e6b 100644 --- a/src/engine/vm/exception.rs +++ b/src/engine/vm/exception.rs @@ -1,20 +1,51 @@ use crate::engine::api::error::Error; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; impl Runtime { + /// Internal-value form of [`Runtime::set_pending_exception`]: consumes the + /// value's edges after the pending-exception root has retained its copy. + pub(crate) fn set_pending_exception_jsvalue(&self, value: JsValue) -> Result<(), RuntimeError> { + let _operation = self.operation(); + let raw = value.as_raw(); + { + let mut state = self.0.state.borrow_mut(); + state.retain_raw_root(&raw)?; + if let Some(previous) = state.pending_exception.replace(raw) { + state.release_owned_raw_root(previous)?; + } + } + // `raw` owns its own retained occurrence; the consumed value's edges + // are no longer needed. + self.release_jsvalue(value)?; + Ok(()) + } + pub(crate) fn set_pending_exception(&self, value: Value) -> Result<(), RuntimeError> { let _operation = self.operation(); self.validate_value_domain(&value, "exception value")?; let raw = self.raw_property_value(&value)?; + // The conversion carries one producer-owned string/BigInt node edge; + // the pending-exception root retains its own occurrence below, so the + // producer edge is released on every exit. + let conversion_edge = raw.conversion_node_edge(); { let mut state = self.0.state.borrow_mut(); - state.retain_raw_root(&raw)?; + if let Err(error) = state.retain_raw_root(&raw) { + drop(state); + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } + return Err(error); + } if let Some(previous) = state.pending_exception.replace(raw) { state.release_owned_raw_root(previous)?; } } + if let Some(edge) = conversion_edge { + self.release_converted_node_edge(edge); + } // `raw` now owns its own retained occurrence. drop(value); Ok(()) @@ -41,6 +72,14 @@ pub(in crate::engine::vm) fn runtime_error_to_vm_error(error: RuntimeError) -> E } } +/// Heap retain/release failures at trusted VM sites carry the same internal +/// diagnostic policy as every other runtime failure. +pub(in crate::engine::vm) fn heap_error_to_vm_error( + error: crate::engine::heap::HeapError, +) -> Error { + runtime_error_to_vm_error(RuntimeError::from(error)) +} + /// Materialize published binding diagnostics outside the resident driver frame. #[inline(never)] pub(super) fn binding_error( @@ -82,7 +121,7 @@ pub(super) fn binding_error( .native_atom_error(kind, prefix, &key, suffix) .map_err(runtime_error_to_vm_error)?; let value = runtime - .new_native_error_from_error(frame.executable.realm, native, &error) + .new_native_error_from_error_jsvalue(frame.executable.realm, native, &error) .map_err(runtime_error_to_vm_error)?; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_instruction(execution.slots.depth(&frame.window)); diff --git a/src/engine/vm/execution.rs b/src/engine/vm/execution.rs index 31c7351d..cc6a9106 100644 --- a/src/engine/vm/execution.rs +++ b/src/engine/vm/execution.rs @@ -3,7 +3,7 @@ use crate::engine::api::error::Error; use crate::engine::api::runtime::Runtime; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::frame::FrameStore; use crate::engine::vm::stack::SlotStore; use std::cell::{Cell, RefCell}; @@ -224,22 +224,40 @@ pub(super) struct RunningExecution { pub query_storage: super::proxy_get_driver::QueryStorage, pub call_storage: super::frame::CallStorage, /// Cold completion owns its payload before the active window is cleared. - pub pending: Option, + pub pending: Option, /// Retained GetField2 result's classification, consumed by the immediate Call. pub selected_native: Option, /// A typed root terminal result; never represented by a manufactured JS Value. pub root_descriptor: Option, pub root_query: Option>, + // The execution never keeps its runtime alive; teardown releases through + // the upgrade only when the runtime still exists. + runtime: std::rc::Weak, _guard: ExecutionGuard, } impl Drop for RunningExecution { fn drop(&mut self) { - drop(self.pending.take()); + let Some(runtime) = self.runtime.upgrade().map(Runtime) else { + // The runtime (and its whole heap) died first; no edge release can + // observe anything. Discard the storage without accounting. + self.pending = None; + self.slots = SlotStore::new(0); + return; + }; + if let Some(pending) = self.pending.take() { + // Teardown cannot report errors; invariant violations surface at + // the deferred-drain boundary like every trusted release. + let _ = runtime.release_jsvalue(pending); + } while let Some(mut frame) = self.frames.pop_current() { // Clear this child's captures and operands while its activation // and every enclosing native query still own their roots. - if self.slots.clear_frame(frame.window.take()).is_err() { + if self + .slots + .clear_frame(&runtime, frame.window.take()) + .is_err() + { // A failed legacy handoff may have detached its Frame before // an allocation failure. Release any remaining arena owners // before unwinding parent native activations; never panic here. @@ -263,6 +281,7 @@ impl RunningExecution { selected_native: None, root_query: None, root_descriptor: None, + runtime: std::rc::Rc::downgrade(&runtime.0), _guard: guard, }) } diff --git a/src/engine/vm/for_in.rs b/src/engine/vm/for_in.rs index fbe398cd..e634c482 100644 --- a/src/engine/vm/for_in.rs +++ b/src/engine/vm/for_in.rs @@ -141,7 +141,7 @@ impl Runtime { } if state .atoms - .array_index(entry.atom)? + .array_index(state.atoms.brand(entry.atom)?)? .is_some_and(|index| index < fast_len) { continue; diff --git a/src/engine/vm/for_in/operation.rs b/src/engine/vm/for_in/operation.rs index 805df74c..5ce23aad 100644 --- a/src/engine/vm/for_in/operation.rs +++ b/src/engine/vm/for_in/operation.rs @@ -4,15 +4,15 @@ use crate::engine::{ atom::PropertyKeyKind, heap::{ContextId, ForInCandidate, ForInProperty}, object::{ObjectRef, PropertyKey}, - value::{JsString, Value, conversion::NativeConversion}, + value::{JsString, JsValue, Value, conversion::NativeConversion}, }; pub(in crate::engine::vm) enum ForInStep { Complete { - value: Value, + value: JsValue, done: Option, }, - Throw(Value), + Throw(JsValue), Keys { object: ObjectRef, resume: ForInResume, @@ -151,11 +151,11 @@ impl ForInStep { match object { Some(object) if fast.is_none() => snapshot(realm, object, AfterSnapshot::Start), object => Ok(ForInStep::Complete { - value: Value::Object(runtime.allocate_for_in_iterator( - object.as_ref(), - fast, - Vec::new(), - )?), + value: JsValue::Object( + runtime + .allocate_for_in_iterator(object.as_ref(), fast, Vec::new())? + .into_handle(), + ), done: None, }), } @@ -187,7 +187,7 @@ fn snapshot( } fn done() -> ForInStep { ForInStep::Complete { - value: Value::Undefined, + value: JsValue::Undefined, done: Some(true), } } @@ -247,7 +247,7 @@ fn advance( if dense_present { record_local_step(); return Ok(ForInStep::Complete { - value: Value::String(name), + value: runtime.unroot_value(&Value::String(name))?, done: Some(false), }); } @@ -286,12 +286,14 @@ fn advance( match reply { NativeConversion::Value(true) => { return Ok(ForInStep::Complete { - value: Value::String(name), + value: runtime.unroot_value(&Value::String(name))?, done: Some(false), }); } NativeConversion::Value(false) => {} - NativeConversion::Throw(value) => return Ok(ForInStep::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(ForInStep::Throw(runtime.into_jsvalue(value)?)); + } } } } @@ -309,7 +311,9 @@ impl ForInResume { ) -> Result { let keys = match reply { NativeConversion::Value(keys) => keys, - NativeConversion::Throw(value) => return Ok(ForInStep::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(ForInStep::Throw(runtime.into_jsvalue(value)?)); + } }; match self.0.phase { Phase::SnapshotKeys { object, after } => snapshot_next( @@ -335,7 +339,9 @@ impl ForInResume { ) -> Result { let value = match reply { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(ForInStep::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(ForInStep::Throw(runtime.into_jsvalue(value)?)); + } }; match self.0.phase { Phase::SnapshotEnumerable { mut snapshot, name } => { @@ -363,7 +369,7 @@ impl ForInResume { Phase::Candidate { iterator, name } => { if value { Ok(ForInStep::Complete { - value: Value::String(name), + value: runtime.unroot_value(&Value::String(name))?, done: Some(false), }) } else { @@ -382,7 +388,9 @@ impl ForInResume { ) -> Result { let prototype = match reply { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(ForInStep::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(ForInStep::Throw(runtime.into_jsvalue(value)?)); + } }; match self.0.phase { Phase::ProbePrototype(probe) => { @@ -437,7 +445,9 @@ fn snapshot_next( &key, )? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(ForInStep::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(ForInStep::Throw(runtime.into_jsvalue(value)?)); + } }; pending .properties @@ -461,11 +471,11 @@ fn snapshot_next( } match pending.after { AfterSnapshot::Start => Ok(ForInStep::Complete { - value: Value::Object(runtime.allocate_for_in_iterator( - Some(&pending.object), - None, - pending.properties, - )?), + value: JsValue::Object( + runtime + .allocate_for_in_iterator(Some(&pending.object), None, pending.properties)? + .into_handle(), + ), done: None, }), AfterSnapshot::Refresh { iterator } => { @@ -516,7 +526,9 @@ fn probe_keys( .internal_snapshot_own_property_is_enumerable(realm, &prototype, &key)? { NativeConversion::Value(value) => value, - NativeConversion::Throw(value) => return Ok(ForInStep::Throw(value)), + NativeConversion::Throw(value) => { + return Ok(ForInStep::Throw(runtime.into_jsvalue(value)?)); + } }; record_local_step(); if enumerable { diff --git a/src/engine/vm/frame.rs b/src/engine/vm/frame.rs index dac6d974..a6ca4321 100644 --- a/src/engine/vm/frame.rs +++ b/src/engine/vm/frame.rs @@ -418,7 +418,7 @@ impl std::ops::DerefMut for FrameCold { mod tests { use super::*; use crate::engine::api::Runtime; - use crate::engine::value::Value; + use crate::engine::value::JsValue; use crate::engine::vm::stack::{FrameStorage, SlotStore}; fn assert_wait_depth_matches_scan(frames: &FrameStore) { @@ -546,13 +546,17 @@ mod tests { let (mut first, mut first_slots) = frame(&runtime, context.realm); first.cold.reusable_captured_locals = vec![true; 23]; let address = &*first.cold as *const FrameBody; - first_slots.clear_frame(first.window.take()).unwrap(); + first_slots + .clear_frame(&runtime, first.window.take()) + .unwrap(); cache.recycle(first.cold); let (flags, grown) = cache.capture_flags(23).unwrap(); assert_eq!(grown, 0); assert_eq!(flags, vec![false; 23]); let (mut second, mut second_slots) = frame(&runtime, context.realm); - second_slots.clear_frame(second.window.take()).unwrap(); + second_slots + .clear_frame(&runtime, second.window.take()) + .unwrap(); let mut contents = second.cold.into_inner(); contents.reusable_captured_locals = flags; let (cold, allocated) = cache.install(contents); @@ -569,6 +573,7 @@ mod tests { let mut slots = SlotStore::new(0); let window = slots .push_frame( + &runtime, &executable.frame_layout(), FrameStorage { original_arguments: Vec::new(), @@ -583,11 +588,12 @@ mod tests { rare: std::cell::OnceCell::new(), return_to: None, entry_guard: None, - input: (CallInput { - this_value: Value::Undefined, - new_target: Value::Undefined, - callee_global: Some(function.clone()), - }) + input: (CallInput::new( + &runtime, + JsValue::Undefined, + JsValue::Undefined, + Some(function.clone()), + )) .into(), function: (function).into(), closure_slots: Default::default(), @@ -706,11 +712,13 @@ mod tests { } let mut child = entry(&runtime, context.realm); let child_object = child.cold.function.object_id(); - child.storage.original_arguments.push(Value::Int(42)); + child.storage.original_arguments.push(JsValue::Int(42)); child .storage .parameters - .push(super::super::bindings::FrameBinding::Direct(Value::Int(42))); + .push(super::super::bindings::FrameBinding::Direct(JsValue::Int( + 42, + ))); let error = push_frame(&mut execution, child).unwrap_err(); assert!(error.to_string().contains(if exhausted_identity { "identity exhausted" @@ -728,7 +736,10 @@ mod tests { execution.frames.next_generation = generation; let replacement = push_frame(&mut execution, entry(&runtime, context.realm)).unwrap(); let mut frame = execution.frames.pop(replacement).unwrap(); - execution.slots.clear_frame(frame.window.take()).unwrap(); + execution + .slots + .clear_frame(&runtime, frame.window.take()) + .unwrap(); let parent = execution.frames.current_mut(parent).unwrap(); assert_eq!( execution.slots.binding_counts(&parent.window).unwrap(), @@ -792,18 +803,21 @@ mod tests { { let runtime = tracked_runtime("child", &events); let context = runtime.new_context(); - let captured_runtime = tracked_runtime("child-slot", &events); - let capture = captured_runtime.new_object(None).unwrap(); + // Internal frame slots hold raw handles without a runtime owner; + // the child frame's cold function root keeps the child runtime + // alive until the frame is abandoned. The slot value belongs to + // the abandoning execution's runtime so its release is valid. + let capture = parent_runtime.new_object(None).unwrap(); let mut child = entry(&runtime, context.realm); child .storage .original_arguments - .push(Value::Object(capture)); + .push(JsValue::Object(capture.into_handle())); child .storage .parameters .push(super::super::bindings::FrameBinding::Direct( - Value::Undefined, + JsValue::Undefined, )); push_frame(&mut execution, child).unwrap(); } @@ -811,6 +825,6 @@ mod tests { drop(parent_runtime); assert!(events.borrow().is_empty()); drop(execution); - assert_eq!(*events.borrow(), ["child-slot", "child", "parent"]); + assert_eq!(*events.borrow(), ["child", "parent"]); } } diff --git a/src/engine/vm/frame/storage.rs b/src/engine/vm/frame/storage.rs index 32d12a73..283d7f7c 100644 --- a/src/engine/vm/frame/storage.rs +++ b/src/engine/vm/frame/storage.rs @@ -426,7 +426,7 @@ impl CallStorage { mod lazy_tests { use super::*; use crate::engine::api::Runtime; - use crate::engine::value::Value; + use crate::engine::value::{JsValue, Value}; use crate::engine::vm::CallInput; #[test] @@ -468,12 +468,7 @@ mod lazy_tests { let mut storage = CallStorage::default(); storage.reserve().unwrap(); let (mut cold, _) = storage.vacant(context.realm); - cold.input = CallInput { - this_value: Value::Undefined, - new_target: Value::Undefined, - callee_global: None, - } - .into(); + cold.input = CallInput::new(&runtime, JsValue::Undefined, JsValue::Undefined, None).into(); assert!(cold.rare.get().is_none()); assert!(cold.input.callee_global.is_none()); let expected = runtime.global_object_for_realm(context.realm).unwrap(); diff --git a/src/engine/vm/frame_exit.rs b/src/engine/vm/frame_exit.rs index 9d5d19d5..b4cba33c 100644 --- a/src/engine/vm/frame_exit.rs +++ b/src/engine/vm/frame_exit.rs @@ -8,7 +8,7 @@ use super::{ }; use crate::engine::{ api::{Error, runtime::Runtime}, - value::Value, + value::JsValue, }; pub(super) struct FrameExit { @@ -18,7 +18,7 @@ pub(super) struct FrameExit { #[inline(never)] pub(super) fn finish( - _runtime: &Runtime, + runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, exit: RunExit, @@ -39,21 +39,23 @@ pub(super) fn finish( .map(Completion::Return) .ok_or_else(|| Error::internal("owned completion has no payload"))?, }; - execution.slots.clear_frame(frame.window.take())?; + execution.slots.clear_frame(runtime, frame.window.take())?; if let Some(guard) = guard { guard.finish().map_err(runtime_error_to_vm_error)?; } execution.call_storage.recycle(frame.cold); let completion = match (completion, constructor_return) { (Completion::Return(value), Some(ConstructorReturn::Base(receiver))) => { - Completion::Return(if matches!(value, Value::Object(_)) { + Completion::Return(if matches!(value, JsValue::Object(_)) { value } else { - receiver + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)? }) } (Completion::Return(value), Some(ConstructorReturn::Derived)) => { - if !matches!(value, Value::Object(_)) { + if !matches!(value, JsValue::Object(_)) { return Err(Error::internal( "derived constructor bytecode returned an unvalidated primitive", )); diff --git a/src/engine/vm/frame_operations.rs b/src/engine/vm/frame_operations.rs index 6d1928e5..843f818d 100644 --- a/src/engine/vm/frame_operations.rs +++ b/src/engine/vm/frame_operations.rs @@ -19,8 +19,8 @@ use super::frame::FrameId; use super::run::RunExit; use crate::engine::api::error::Error; use crate::engine::api::runtime::Runtime; -use crate::engine::value::Value; use crate::engine::value::conversion::NativeConversion; +use crate::engine::value::{JsValue, Value}; #[inline(never)] pub(super) fn pure( @@ -59,7 +59,7 @@ pub(super) fn home_object( let depth = execution.slots.depth(&frame.window); execution .slots - .push(&mut frame.window, Value::Object(home))?; + .push(&mut frame.window, JsValue::Object(home.into_handle()))?; frame.resume_pc = frame .fault_pc .checked_add(1) @@ -77,24 +77,30 @@ pub(super) fn get_super( ) -> Result { let frame = execution.frames.current_mut(id)?; let value = execution.slots.peek(&frame.window, 0)?; - if let Value::Object(object) = value { - if object.belongs_to(runtime) { - let value = runtime - .get_prototype_of(object) - .map_err(runtime_error_to_vm_error)? - .map_or(Value::Null, Value::Object); - #[cfg(feature = "profiling")] - let depth = execution.slots.depth(&frame.window); - execution.slots.pop(&mut frame.window)?; - execution.slots.push(&mut frame.window, value)?; - frame.resume_pc = frame - .fault_pc - .checked_add(1) - .ok_or_else(|| Error::internal("super resume PC overflow"))?; - #[cfg(feature = "profiling")] - crate::engine::api::profiling::record_owned_instruction(depth); - return Ok(CallStep::Entered); + let object = match value { + JsValue::Object(id) => { + crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), *id).ok() } + _ => None, + }; + if let Some(object) = object { + let prototype = runtime + .get_prototype_of(&object) + .map_err(runtime_error_to_vm_error)? + .map_or(JsValue::Null, |prototype| { + JsValue::Object(prototype.into_handle()) + }); + #[cfg(feature = "profiling")] + let depth = execution.slots.depth(&frame.window); + execution.slots.pop(&mut frame.window)?; + execution.slots.push(&mut frame.window, prototype)?; + frame.resume_pc = frame + .fault_pc + .checked_add(1) + .ok_or_else(|| Error::internal("super resume PC overflow"))?; + #[cfg(feature = "profiling")] + crate::engine::api::profiling::record_owned_instruction(depth); + return Ok(CallStep::Entered); } Ok(CallStep::Bridge) } @@ -160,14 +166,19 @@ pub(super) fn for_in( let realm = frame.executable.realm; let depth = execution.slots.depth(&frame.window); let step = if next { - let Value::Object(iterator) = execution.slots.peek(&frame.window, 0)? else { + let JsValue::Object(id) = execution.slots.peek(&frame.window, 0)? else { return Err(Error::internal( "for-in next received a non-object iterator", )); }; - super::for_in::operation::ForInStep::next(runtime, realm, iterator) + let iterator = crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), *id) + .map_err(|error| runtime_error_to_vm_error(error.into()))?; + super::for_in::operation::ForInStep::next(runtime, realm, &iterator) } else { let value = execution.slots.pop(&mut frame.window)?; + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; super::for_in::operation::ForInStep::start(runtime, realm, value) }; match step { @@ -192,12 +203,12 @@ pub(super) fn numeric( #[inline(never)] pub(super) fn strict_equality( - _runtime: &Runtime, + runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, negate: bool, ) -> Result { - super::run::strict_comparison(execution, id, negate)?; + super::run::strict_comparison(runtime, execution, id, negate)?; Ok(CallStep::Entered) } @@ -223,7 +234,12 @@ pub(super) fn set_name( ) -> Result { match super::property_keys::set_name(runtime, execution, id, index)? { None => Ok(CallStep::Entered), - Some(value) => Ok(CallStep::Complete(Completion::Throw(value))), + Some(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + Ok(CallStep::Complete(Completion::Throw(value))) + } } } @@ -293,7 +309,7 @@ pub(super) fn close_captured( #[inline(never)] pub(super) fn catch( - _runtime: &Runtime, + runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, exit: RunExit, @@ -345,7 +361,10 @@ pub(super) fn catch( prepare_captured_reuse(frame, &execution.slots)?; let value = execution.slots.pop(&mut frame.window)?; while execution.slots.depth(&frame.window) > stack_depth { - drop(execution.slots.pop(&mut frame.window)?); + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; } execution.slots.push(&mut frame.window, value)?; } @@ -440,7 +459,7 @@ pub(super) fn binding( let depth = execution.slots.depth(&frame.window); let value = if write { Some(if keep { - super::stack::copy_value(execution.slots.peek(&frame.window, 0)?)? + super::stack::copy_value(runtime, execution.slots.peek(&frame.window, 0)?)? } else { execution.slots.pop(&mut frame.window)? }) @@ -487,7 +506,7 @@ pub(super) fn binding( return Err(error); }; let value = runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?; Ok(CallStep::Complete(Completion::Throw(value))) } @@ -510,7 +529,7 @@ pub(super) fn lexical_uninitialized( !frame.executable.metadata.strip_variable_debug, )?; let value = runtime - .new_native_error_from_error( + .new_native_error_from_error_jsvalue( frame.executable.realm, crate::engine::api::error::NativeErrorKind::Reference, &error, @@ -569,7 +588,7 @@ pub(super) fn initialize_derived( return Err(error); }; let value = runtime - .new_native_error_from_error(frame.executable.realm, kind, &error) + .new_native_error_from_error_jsvalue(frame.executable.realm, kind, &error) .map_err(runtime_error_to_vm_error)?; Ok(CallStep::Complete(Completion::Throw(value))) } @@ -617,8 +636,11 @@ pub(super) fn normalize_this( let frame = execution.frames.current_mut(id)?; // This conversion only allocates a primitive wrapper; it cannot // call JavaScript. Keep its identity across every later handoff. + let this_value = runtime + .root_value(&frame.cold.input.this_value) + .map_err(runtime_error_to_vm_error)?; let value = runtime - .native_to_object(frame.executable.realm, frame.cold.input.this_value.clone()) + .native_to_object(frame.executable.realm, this_value) .map_err(runtime_error_to_vm_error)?; let NativeConversion::Value(object) = value else { return Err(Error::internal("non-null primitive this boxing threw")); diff --git a/src/engine/vm/frame_operations/numeric.rs b/src/engine/vm/frame_operations/numeric.rs index 54423f79..5c17856c 100644 --- a/src/engine/vm/frame_operations/numeric.rs +++ b/src/engine/vm/frame_operations/numeric.rs @@ -25,8 +25,8 @@ impl NumericProgress { pub(in crate::engine::vm) fn commit_output( execution: &mut RunningExecution, id: FrameId, - value: crate::engine::value::Value, - previous: Option, + value: crate::engine::value::JsValue, + previous: Option, _depth: usize, ) -> Result<(), Error> { let frame = execution.frames.current_mut(id)?; @@ -54,7 +54,7 @@ pub(in crate::engine::vm) fn try_complete_primitive( id: FrameId, kind: NumericKind, ) -> Result, Error> { - use crate::engine::value::Value; + use crate::engine::value::JsValue; let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; let depth = execution.slots.depth(&frame.window); @@ -64,7 +64,7 @@ pub(in crate::engine::vm) fn try_complete_primitive( // A malformed stack declines untouched: the canonical outer entry must // still pop RHS before reporting a missing LHS. for offset in 0..if kind.unary() { 1 } else { 2 } { - if matches!(slots.peek(offset), Err(_) | Ok(Value::Object(_))) { + if matches!(slots.peek(offset), Err(_) | Ok(JsValue::Object(_))) { return Ok(None); } } @@ -79,7 +79,7 @@ pub(in crate::engine::vm) fn try_complete_primitive( } }; if !kind.primitive_arithmetic() { - return match NumericStep::start(kind, left, right) { + return match NumericStep::start(runtime, kind, left, right) { Ok(step) => crate::engine::vm::proxy_get_driver::start_numeric( runtime, execution, id, step, depth, ) @@ -88,13 +88,14 @@ pub(in crate::engine::vm) fn try_complete_primitive( .map(|step| Some(NumericProgress::Deferred(step))), }; } - let output = match crate::engine::vm::numeric::operation::primitive_output(kind, left, right) { - Ok(output) => output, - Err(error) => { - return crate::engine::vm::property_driver::throw_error(runtime, realm, error) - .map(|step| Some(NumericProgress::Deferred(step))); - } - }; + let output = + match crate::engine::vm::numeric::operation::primitive_output(runtime, kind, left, right) { + Ok(output) => output, + Err(error) => { + return crate::engine::vm::property_driver::throw_error(runtime, realm, error) + .map(|step| Some(NumericProgress::Deferred(step))); + } + }; #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event("numeric_completed_without_query"); let mut value = Some(output.value); @@ -129,9 +130,17 @@ pub(in crate::engine::vm) fn complete( (execution.slots.pop(&mut frame.window)?, None) } else { let right = execution.slots.pop(&mut frame.window)?; - (execution.slots.pop(&mut frame.window)?, Some(right)) + match execution.slots.pop(&mut frame.window) { + Ok(left) => (left, Some(right)), + Err(error) => { + runtime + .release_jsvalue(right) + .map_err(crate::engine::vm::exception::runtime_error_to_vm_error)?; + return Err(error); + } + } }; - let result = match NumericStep::start(kind, left, right) { + let result = match NumericStep::start(runtime, kind, left, right) { Ok(step) => { crate::engine::vm::proxy_get_driver::start_numeric(runtime, execution, id, step, depth)? } @@ -150,6 +159,7 @@ pub(in crate::engine::vm) fn complete( #[cfg(test)] mod tests { use crate::engine::api::{Runtime, Value}; + use crate::engine::value::JsValue; use super::*; use crate::engine::vm::{ @@ -207,7 +217,7 @@ mod tests { (execution, id) } - fn push(execution: &mut RunningExecution, id: FrameId, value: Value) { + fn push(execution: &mut RunningExecution, id: FrameId, value: JsValue) { let frame = execution.frames.current_mut(id).unwrap(); execution.slots.push(&mut frame.window, value).unwrap(); } @@ -217,7 +227,7 @@ mod tests { let runtime = Runtime::new(); let mut context = runtime.new_context(); let (mut execution, id) = fixture(&runtime, &mut context); - push(&mut execution, id, Value::Int(7)); + push(&mut execution, id, JsValue::Int(7)); assert!( try_complete_primitive(&runtime, &mut execution, id, NumericKind::Mul) .unwrap() @@ -237,7 +247,7 @@ mod tests { let frame = execution.frames.current_mut(id).unwrap(); if execution .slots - .push(&mut frame.window, Value::Int(0)) + .push(&mut frame.window, JsValue::Int(0)) .is_err() { break; @@ -248,12 +258,19 @@ mod tests { let fault = frame.fault_pc; let resume = frame.resume_pc; assert!( - commit_output(&mut execution, id, Value::Int(99), Some(Value::Int(41)), 0).is_err() + commit_output( + &mut execution, + id, + JsValue::Int(99), + Some(JsValue::Int(41)), + 0 + ) + .is_err() ); let frame = execution.frames.current_mut(id).unwrap(); assert_eq!( execution.slots.peek(&frame.window, 0).unwrap(), - &Value::Int(41) + &JsValue::Int(41) ); assert_eq!((frame.fault_pc, frame.resume_pc), (fault, resume)); } @@ -266,7 +283,11 @@ mod tests { push( &mut execution, id, - Value::String(crate::engine::value::JsString::from_static("7")), + runtime + .into_jsvalue(Value::String(crate::engine::value::JsString::from_static( + "7", + ))) + .unwrap(), ); { let frame = execution.frames.current_mut(id).unwrap(); @@ -300,7 +321,7 @@ mod tests { let frame = execution.frames.current_mut(id).unwrap(); if execution .slots - .push(&mut frame.window, Value::Int(0)) + .push(&mut frame.window, JsValue::Int(0)) .is_err() { break; @@ -308,7 +329,10 @@ mod tests { } let frame = execution.frames.current_mut(id).unwrap(); execution.slots.pop(&mut frame.window).unwrap(); - execution.slots.push(&mut frame.window, text).unwrap(); + execution + .slots + .push(&mut frame.window, runtime.into_jsvalue(text).unwrap()) + .unwrap(); let depth = execution.slots.depth(&frame.window); let before = (frame.fault_pc, frame.resume_pc); let realm = frame.executable.realm; @@ -345,7 +369,7 @@ mod tests { let slots = transaction.slots(); assert_eq!( slots.peek(0).unwrap(), - &Value::Int(41), + &JsValue::Int(41), "previous commits before value fails" ); } @@ -367,8 +391,8 @@ mod tests { .current_mut(id) .unwrap() .property_generation = u64::MAX; - push(&mut execution, id, Value::Int(6)); - push(&mut execution, id, Value::Int(7)); + push(&mut execution, id, JsValue::Int(6)); + push(&mut execution, id, JsValue::Int(7)); assert!(matches!( try_complete_primitive(&runtime, &mut execution, id, NumericKind::Mul).unwrap(), Some(NumericProgress::Completed) @@ -377,24 +401,29 @@ mod tests { assert_eq!(frame.property_generation, u64::MAX); assert_eq!( execution.slots.pop(&mut frame.window).unwrap(), - Value::Int(42) + JsValue::Int(42) ); assert!(matches!( crate::engine::vm::proxy_get_driver::start_numeric( &runtime, &mut execution, id, - NumericStep::Throw(Value::Int(17)), + NumericStep::Throw(JsValue::Int(17)), 0 ) .unwrap(), NumericProgress::Deferred(CallStep::Complete(crate::engine::vm::Completion::Throw( - Value::Int(17) + JsValue::Int(17) ))) )); let object = runtime.new_object(None).unwrap(); - let step = - NumericStep::start(NumericKind::Plus, Value::Object(object.clone()), None).unwrap(); + let step = NumericStep::start( + &runtime, + NumericKind::Plus, + JsValue::Object(object.clone().into_handle()), + None, + ) + .unwrap(); assert!( crate::engine::vm::proxy_get_driver::start_numeric( &runtime, @@ -405,7 +434,7 @@ mod tests { ) .is_err() ); - push(&mut execution, id, Value::Object(object)); + push(&mut execution, id, JsValue::Object(object.into_handle())); let mut identity = u64::MAX; assert!(complete_primitives(&runtime, &mut execution, id, false, &mut identity).is_err()); let frame = execution.frames.current_mut(id).unwrap(); @@ -422,16 +451,18 @@ mod tests { push( &mut execution, id, - Value::Object(foreign.new_object(None).unwrap()), + JsValue::Object(foreign.new_object(None).unwrap().into_handle()), ); - identity = u64::MAX; - let error = complete_primitives(&runtime, &mut execution, id, false, &mut identity) - .err() - .expect("foreign conversion operand must fail before identity issue"); - assert!(error.to_string().contains("conversion operand")); - assert_eq!(identity, u64::MAX); + identity = 10; + assert!(matches!( + complete_primitives(&runtime, &mut execution, id, false, &mut identity).unwrap(), + PrimitiveCompletion::Declined + )); + assert_eq!(identity, 11); let frame = execution.frames.current_mut(id).unwrap(); - assert_eq!(execution.slots.depth(&frame.window), 1); + let pending = execution.slots.pop(&mut frame.window).unwrap(); + foreign.release_jsvalue(pending).unwrap(); + assert_eq!(execution.slots.depth(&frame.window), 0); } #[test] @@ -471,8 +502,16 @@ mod tests { .current_mut(id) .unwrap() .property_generation = u64::MAX; - push(&mut execution, id, Value::Object(target.clone())); - push(&mut execution, id, source.clone()); + push( + &mut execution, + id, + JsValue::Object(target.clone().into_handle()), + ); + push( + &mut execution, + id, + runtime.into_jsvalue(source.clone()).unwrap(), + ); let result = crate::engine::vm::proxy_get_driver::start_object_copy( &runtime, &mut execution, @@ -486,7 +525,12 @@ mod tests { ); let frame = execution.frames.current_mut(id).unwrap(); assert_eq!(execution.slots.depth(&frame.window), 2); - assert_eq!(execution.slots.peek(&frame.window, 0).unwrap(), &source); + assert_eq!( + runtime + .root_value(execution.slots.peek(&frame.window, 0).unwrap()) + .unwrap(), + source + ); assert_eq!(frame.property_generation, u64::MAX); // Classification may already have completed ordinary fresh-target // definitions. No selected getter was called or replayed to discover it. @@ -521,8 +565,8 @@ mod tests { .current_mut(id) .unwrap() .property_generation = u64::MAX; - push(&mut execution, id, target); - push(&mut execution, id, source); + push(&mut execution, id, runtime.into_jsvalue(target).unwrap()); + push(&mut execution, id, runtime.into_jsvalue(source).unwrap()); let result = crate::engine::vm::proxy_get_driver::start_object_copy( &runtime, &mut execution, diff --git a/src/engine/vm/generator.rs b/src/engine/vm/generator.rs index a772aeb9..042c9bee 100644 --- a/src/engine/vm/generator.rs +++ b/src/engine/vm/generator.rs @@ -18,7 +18,7 @@ use crate::engine::heap::{ use crate::engine::object::{ DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, }; -use crate::engine::value::{JsString, Value}; +use crate::engine::value::{JsString, JsValue, Value}; use crate::engine::vm::call::{NativeArguments, NativeInvocation, NativeInvokeOutcome}; use crate::engine::vm::suspend::{self, EncodedVmActivation, VmActivationResume, VmRunOutcome}; use crate::engine::vm::{Completion, VmResume, VmSuspendKind}; @@ -147,9 +147,12 @@ impl Runtime { pub(super) fn allocate_generator_object( &self, prototype: &ObjectRef, - activation: EncodedVmActivation, + mut activation: EncodedVmActivation, ) -> Result { - let atoms = activation.atoms(); + let atoms = { + let state = self.0.state.borrow(); + activation.atoms(&state.atoms)? + }; let mut state = self.0.state.borrow_mut(); let shape = state.get_or_create_shape(Some(prototype.object_id()), &[])?; let mut retained_atoms = Vec::with_capacity(atoms.len()); @@ -158,6 +161,7 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + activation.release_conversion_edges(self); return Err(error.into()); } retained_atoms.push(atom); @@ -172,11 +176,15 @@ impl Runtime { state.release_atoms(retained_atoms)?; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + activation.release_conversion_edges(self); return Err(error.into()); } }; let cleanup = state.heap.release_shape(shape)?; state.apply_cleanup(cleanup)?; + // The generator object retained its own activation edges, so the + // caller-owned conversion edges move to the owner and can drop. + activation.release_conversion_edges(self); drop(state); drop(activation); Ok(ObjectRef::from_owned_handle(self.clone(), object)) @@ -191,9 +199,13 @@ impl Runtime { ) -> Result { match self.call_generator_prototype_resume_raw(realm, kind, invocation, arguments)? { NativeInvokeOutcome::Completion(completion) => Ok(completion), - NativeInvokeOutcome::IteratorNextRaw { value, done } => Ok(Completion::Return( - Value::Object(self.new_iterator_result(realm, value, done)?), - )), + NativeInvokeOutcome::IteratorNextRaw { value, done } => { + let value = self.root_and_release_jsvalue(value)?; + let result = self.new_iterator_result(realm, value, done)?; + Ok(Completion::Return( + self.into_jsvalue(Value::Object(result))?, + )) + } } } @@ -223,13 +235,14 @@ impl Runtime { let argument = arguments .readable .first() - .cloned() + .map(|value| self.dup_jsvalue(value)) + .transpose()? .ok_or(RuntimeError::Invariant( "Generator resume has no readable argument slot", ))?; - let Value::Object(generator) = this_value else { + let Value::Object(generator) = self.root_and_release_jsvalue(this_value)? else { return Ok(GeneratorStep::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(self.new_native_error( + Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not a generator", @@ -249,7 +262,7 @@ impl Runtime { }; let Some((previous_state, activation)) = snapshot else { return Ok(GeneratorStep::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(self.new_native_error( + Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "not a generator", @@ -274,7 +287,7 @@ impl Runtime { )); } return Ok(GeneratorStep::Complete(NativeInvokeOutcome::Completion( - Completion::Throw(self.new_native_error( + Completion::Throw(self.new_native_error_jsvalue( realm, NativeErrorKind::Type, "cannot invoke a running generator", @@ -379,7 +392,7 @@ impl Runtime { } VmRunOutcome::Suspend { value: yielded, - activation, + mut activation, } => { let state = match activation.kind { VmSuspendKind::Yield => GeneratorState::SuspendedYield, @@ -394,14 +407,14 @@ impl Runtime { } }; if state == GeneratorState::SuspendedYieldStar - && !matches!(yielded, Value::Object(_)) + && !matches!(yielded, JsValue::Object(_)) { self.complete_executing_generator(generator)?; return Err(RuntimeError::Invariant( "yield* suspension did not retain an iterator-result object", )); } - if let Err(error) = self.store_generator_suspension(generator, &activation) { + if let Err(error) = self.store_generator_suspension(generator, &mut activation) { let _ = self.complete_executing_generator(generator); return Err(error); } @@ -429,11 +442,11 @@ impl Runtime { fn completed_generator_outcome( kind: GeneratorResumeKind, - argument: Value, + argument: JsValue, ) -> NativeInvokeOutcome { match kind { GeneratorResumeKind::Next => NativeInvokeOutcome::IteratorNextRaw { - value: Value::Undefined, + value: JsValue::Undefined, done: true, }, GeneratorResumeKind::Return => NativeInvokeOutcome::IteratorNextRaw { @@ -449,7 +462,7 @@ impl Runtime { fn store_generator_suspension( &self, generator: &ObjectRef, - activation: &EncodedVmActivation, + activation: &mut EncodedVmActivation, ) -> Result<(), RuntimeError> { let generator_state = match activation.kind { VmSuspendKind::Yield => GeneratorState::SuspendedYield, @@ -461,12 +474,16 @@ impl Runtime { )); } }; - let atoms = activation.atoms(); + let atoms = { + let state = self.0.state.borrow(); + activation.atoms(&state.atoms)? + }; let mut state = self.0.state.borrow_mut(); let mut retained_atoms = Vec::with_capacity(atoms.len()); for atom in atoms { if let Err(error) = state.atoms.retain(atom) { state.release_atoms(retained_atoms)?; + activation.release_conversion_edges(self); return Err(error.into()); } retained_atoms.push(atom); @@ -477,8 +494,12 @@ impl Runtime { activation.data.clone(), ) { state.release_atoms(retained_atoms)?; + activation.release_conversion_edges(self); return Err(error.into()); } + // The heap record retained its own activation edges, so the + // caller-owned conversion edges can drop. + activation.release_conversion_edges(self); Ok(()) } @@ -959,12 +980,15 @@ mod tests { }, ) .unwrap(); - assert_eq!( - runtime - .call_internal(context.realm, &callable, Value::Undefined, &[]) - .unwrap(), - Completion::Throw(marker) - ); + let thrown = match runtime + .call_internal(context.realm, &callable, Value::Undefined, &[]) + .unwrap() + { + Completion::Throw(value) => value, + other => panic!("expected generator creation to throw, got {other:?}"), + }; + assert_eq!(runtime.root_value(&thrown).unwrap(), marker); + runtime.release_jsvalue(thrown).unwrap(); assert_eq!(context.eval("__order").unwrap(), Value::Int(1)); assert!(runtime.0.state.borrow().active_frames.is_empty()); } diff --git a/src/engine/vm/iterator_driver.rs b/src/engine/vm/iterator_driver.rs index 218f40c7..f80add18 100644 --- a/src/engine/vm/iterator_driver.rs +++ b/src/engine/vm/iterator_driver.rs @@ -4,7 +4,7 @@ pub(super) mod suspension; use super::{ Completion, driver::CallStep, - exception::runtime_error_to_vm_error, + exception::{heap_error_to_vm_error, runtime_error_to_vm_error}, execution::RunningExecution, frame::{FrameId, OperationTarget, ReturnTarget}, }; @@ -16,7 +16,7 @@ use crate::engine::{ CallableRef, DescriptorField, ObjectRef, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol, operations::PropertyDefineOutcome, }, - value::Value, + value::JsValue, }; pub(super) use regions::unwind; @@ -86,40 +86,40 @@ impl std::ops::DerefMut for PendingIterator { const _: () = assert!(size_of::() <= 8); pub(super) struct PendingIteratorState { mode: Mode, - yielded: Value, + yielded: JsValue, done: bool, frame: FrameId, pc: usize, generation: u64, realm: ContextId, array: Option, - iterable: Value, + iterable: JsValue, position: u32, stage: Stage, builtin_probe: bool, - iterator: Value, - next: Value, - fast: Option>, + iterator: JsValue, + next: JsValue, + fast: Option>, ready: bool, - abrupt: Option, - argument: Value, + abrupt: Option, + argument: JsValue, sync_fallback: bool, } /// A query driver consumes these actions in its existing dispatch loop. pub(super) enum IteratorAction { - Read(Value, PropertyKey), - Call(CallableRef, Value), - Invoke(super::call::DirectCallTarget, Value, Vec), - Next(CallableRef, Value), + Read(JsValue, PropertyKey), + Call(CallableRef, JsValue), + Invoke(super::call::DirectCallTarget, JsValue, Vec), + Next(CallableRef, JsValue), Finish, } enum Action { - Read(Value, PropertyKey), - Call(CallableRef, Value), - Invoke(super::call::DirectCallTarget, Value, Vec), - Next(CallableRef, Value), + Read(JsValue, PropertyKey), + Call(CallableRef, JsValue), + Invoke(super::call::DirectCallTarget, JsValue, Vec), + Next(CallableRef, JsValue), Reply(Completion), Finish, } @@ -131,17 +131,14 @@ pub(super) fn start( id: FrameId, ) -> Result { let frame = execution.frames.current_mut(id)?; - let Value::Object(array) = execution.slots.peek(&frame.window, 2)? else { + let JsValue::Object(array) = execution.slots.peek(&frame.window, 2)? else { return Ok(CallStep::Bridge); }; - if !array.belongs_to(runtime) { - return Ok(CallStep::Bridge); - } { let state = runtime.0.state.borrow(); let object = state .heap - .object(array.object_id()) + .object(*array) .map_err(|e| Error::internal(e.to_string()))?; if !matches!( (object.kind, &object.payload), @@ -151,12 +148,15 @@ pub(super) fn start( return Ok(CallStep::Bridge); } } - let Value::Int(position) = execution.slots.peek(&frame.window, 1)? else { + let JsValue::Int(position) = execution.slots.peek(&frame.window, 1)? else { return Ok(CallStep::Bridge); }; - let array = array.clone(); + let array = crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), *array) + .map_err(heap_error_to_vm_error)?; let position = *position as u32; - let iterable = execution.slots.peek(&frame.window, 0)?.clone(); + let iterable = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?; let mut pending = PendingIteratorState::new(frame, id, Mode::Append)?; pending.array = Some(array); pending.position = position; @@ -209,40 +209,52 @@ pub(super) fn operation( )); } if !enabled { - return finish_next(execution, id, record_base, Value::Undefined, true, None); + return finish_next( + runtime, + execution, + id, + record_base, + JsValue::Undefined, + true, + None, + ); } // The record already owns receiver and captured method. Classify // before building a general iterator operation or its waiting box. let next = execution.slots.peek(&frame.window, offset)?; - let metadata = if let Value::Object(method) = next { - if method.belongs_to(runtime) { - let state = runtime.0.state.borrow(); - match &state - .heap - .object(method.object_id()) - .map_err(|error| runtime_error_to_vm_error(error.into()))? - .payload + let metadata = if let JsValue::Object(method) = next { + let state = runtime.0.state.borrow(); + match &state + .heap + .object(*method) + .map_err(|error| runtime_error_to_vm_error(error.into()))? + .payload + { + ObjectPayload::NativeFunction { data, .. } + if data.target == NativeFunctionId::ArrayIteratorNext => { - ObjectPayload::NativeFunction { data, .. } - if data.target == NativeFunctionId::ArrayIteratorNext => - { - Some(()) - } - _ => None, + Some(()) } - } else { - None + _ => None, } } else { None }; if metadata.is_some() { - let callable = callable(runtime, next.clone(), "not a function")?; + let callable = callable( + runtime, + runtime + .dup_jsvalue(next) + .map_err(runtime_error_to_vm_error)?, + "not a function", + )?; let (_, defining_realm, min_readable_args) = runtime .direct_native_callable_metadata(&callable) .map_err(runtime_error_to_vm_error)? .ok_or_else(|| Error::internal("Array-next lost native metadata"))?; - let iterator = execution.slots.peek(&frame.window, offset + 1)?.clone(); + let iterator = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, offset + 1)?) + .map_err(runtime_error_to_vm_error)?; return super::proxy_get_driver::start_array_next_without_pending( runtime, execution, @@ -255,8 +267,12 @@ pub(super) fn operation( ); } let mut pending = PendingIteratorState::new(frame, id, Mode::Next { record_base })?; - pending.iterator = execution.slots.peek(&frame.window, offset + 1)?.clone(); - pending.next = execution.slots.peek(&frame.window, offset)?.clone(); + pending.iterator = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, offset + 1)?) + .map_err(runtime_error_to_vm_error)?; + pending.next = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, offset)?) + .map_err(runtime_error_to_vm_error)?; pending.stage = if enabled { Stage::Next } else { Stage::Finish }; pending.done = !enabled; pending @@ -265,7 +281,7 @@ pub(super) fn operation( let preserve = operation != Operation::Close; let instruction_depth = execution.slots.depth(&frame.window); let (iterator, enabled, asynchronous) = - regions::take(frame, &mut execution.slots, preserve)?; + regions::take(runtime, frame, &mut execution.slots, preserve)?; if asynchronous && !enabled { return Err(Error::internal( "synchronous cleanup targeted a pending async iterator", @@ -302,7 +318,7 @@ pub(super) fn operation( runtime, execution, pending, - Some(Completion::Return(Value::Undefined)), + Some(Completion::Return(JsValue::Undefined)), ) } @@ -310,8 +326,8 @@ fn close_unwind( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - iterator: Value, - value: Value, + iterator: JsValue, + value: JsValue, ) -> Result { let frame = execution.frames.current_mut(id)?; let mut pending = PendingIteratorState::new( @@ -328,18 +344,20 @@ fn close_unwind( runtime, execution, pending, - Some(Completion::Return(Value::Undefined)), + Some(Completion::Return(JsValue::Undefined)), ) } pub(super) fn finish( + runtime: &Runtime, execution: &mut RunningExecution, mut pending: PendingIterator, ) -> Result { - finish_local(execution, &mut pending.0) + finish_local(runtime, execution, &mut pending.0) } fn finish_local( + runtime: &Runtime, execution: &mut RunningExecution, pending: &mut PendingIteratorState, ) -> Result { @@ -353,21 +371,26 @@ fn finish_local( match pending.mode { Mode::Append => { for _ in 0..3 { - execution.slots.pop(&mut frame.window)?; + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(runtime_error_to_vm_error)?; } if pending.abrupt.is_none() { - execution.slots.push( - &mut frame.window, - Value::Object( - pending - .array - .take() - .ok_or_else(|| Error::internal("Append lost its target"))?, - ), - )?; + let array = pending + .array + .take() + .ok_or_else(|| Error::internal("Append lost its target"))?; + let id = array.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; execution .slots - .push(&mut frame.window, Value::Int(pending.position as i32))?; + .push(&mut frame.window, JsValue::Object(id))?; + execution + .slots + .push(&mut frame.window, JsValue::Int(pending.position as i32))?; } } Mode::Start { @@ -394,14 +417,16 @@ fn finish_local( } execution.slots.push( &mut frame.window, - std::mem::replace(&mut pending.iterator, Value::Undefined), + std::mem::replace(&mut pending.iterator, JsValue::Undefined), )?; execution.slots.push( &mut frame.window, - std::mem::replace(&mut pending.next, Value::Undefined), + std::mem::replace(&mut pending.next, JsValue::Undefined), )?; if delegating { - execution.slots.push(&mut frame.window, Value::Undefined)?; + execution + .slots + .push(&mut frame.window, JsValue::Undefined)?; } else { frame.cold.regions.push(super::VmUnwindRegion::Iterator { record_base, @@ -415,22 +440,25 @@ fn finish_local( if pending.abrupt.is_none() { execution.slots.push( &mut frame.window, - std::mem::replace(&mut pending.yielded, Value::Undefined), + std::mem::replace(&mut pending.yielded, JsValue::Undefined), )?; } } Mode::Delegate(_) => { if pending.abrupt.is_none() { if !pending.done { - execution.slots.replace_operand( + let old = execution.slots.replace_operand( &frame.window, 0, - std::mem::replace(&mut pending.yielded, Value::Undefined), + std::mem::replace(&mut pending.yielded, JsValue::Undefined), )?; + runtime + .release_jsvalue(old) + .map_err(runtime_error_to_vm_error)?; } execution .slots - .push(&mut frame.window, Value::Bool(pending.done))?; + .push(&mut frame.window, JsValue::Bool(pending.done))?; } } Mode::Parse { record_base } => { @@ -438,25 +466,27 @@ fn finish_local( suspension::enable(frame, record_base)?; execution.slots.push( &mut frame.window, - std::mem::replace(&mut pending.yielded, Value::Undefined), + std::mem::replace(&mut pending.yielded, JsValue::Undefined), )?; execution .slots - .push(&mut frame.window, Value::Bool(pending.done))?; + .push(&mut frame.window, JsValue::Bool(pending.done))?; } } Mode::Next { record_base } => { apply_next( + runtime, frame, &mut execution.slots, record_base, - std::mem::replace(&mut pending.yielded, Value::Undefined), + std::mem::replace(&mut pending.yielded, JsValue::Undefined), pending.done, pending.abrupt.is_some(), )?; } Mode::Close { .. } => {} } + release_pending_edges(runtime, pending)?; if let Some(value) = pending.abrupt.take() { return Ok(CallStep::Complete(Completion::Throw(value))); } @@ -469,37 +499,61 @@ fn finish_local( Ok(CallStep::Entered) } +/// Release owned edges the suspended state still holds after its mode-specific +/// transfer. Fields moved into operand slots are `Undefined` by this point, so +/// the helper is idempotent across every finish mode. +fn release_pending_edges( + runtime: &Runtime, + pending: &mut PendingIteratorState, +) -> Result<(), Error> { + for value in [ + std::mem::replace(&mut pending.iterable, JsValue::Undefined), + std::mem::replace(&mut pending.iterator, JsValue::Undefined), + std::mem::replace(&mut pending.next, JsValue::Undefined), + std::mem::replace(&mut pending.yielded, JsValue::Undefined), + std::mem::replace(&mut pending.argument, JsValue::Undefined), + ] { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + } + Ok(()) +} + fn apply_next( + runtime: &Runtime, frame: &mut super::frame::Frame, slots: &mut super::stack::SlotStore, record_base: usize, - value: Value, + value: JsValue, done: bool, abrupt: bool, ) -> Result<(), Error> { if done || abrupt { - regions::disable(frame, slots, record_base)?; + regions::disable(runtime, frame, slots, record_base)?; } if !abrupt { let mut window = slots.run_window(&mut frame.window)?; window.push(value)?; - window.push(Value::Bool(done))?; + window.push(JsValue::Bool(done))?; } Ok(()) } pub(super) fn finish_next( + runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, record_base: usize, - value: Value, + value: JsValue, done: bool, - abrupt: Option, + abrupt: Option, ) -> Result { let frame = execution.frames.current_mut(id)?; #[cfg(feature = "profiling")] let depth = execution.slots.depth(&frame.window); apply_next( + runtime, frame, &mut execution.slots, record_base, @@ -560,7 +614,7 @@ fn materialize(runtime: &Runtime, realm: ContextId, error: Error) -> Result Result { let action = state.advance_query(runtime, response)?; if matches!(action, IteratorAction::Finish) { - return finish_local(execution, &mut state); + return finish_local(runtime, execution, &mut state); } dispatch_action(runtime, execution, state.into_resident(), action) } @@ -594,7 +648,7 @@ fn dispatch_action( action: IteratorAction, ) -> Result { match action { - IteratorAction::Finish => finish(execution, pending), + IteratorAction::Finish => finish(runtime, execution, pending), IteratorAction::Read(base, key) => { super::proxy_get_driver::start_iterator_read(runtime, execution, pending, base, key) } @@ -637,13 +691,13 @@ impl PendingIteratorState { Action::Reply(completion) => response = Some(completion), Action::Finish => return Ok(IteratorAction::Finish), Action::Read(base, key) => { - if matches!(base, Value::Null | Value::Undefined) { + if matches!(base, JsValue::Null | JsValue::Undefined) { response = Some(materialize( runtime, self.realm, Error::new( ErrorKind::Type, - if matches!(base, Value::Null) { + if matches!(base, JsValue::Null) { "cannot read property of null" } else { "cannot read property of undefined" @@ -705,23 +759,23 @@ impl PendingIteratorState { .ok_or_else(|| Error::internal("iterator operation identity exhausted"))?; let pending = Self { mode, - yielded: Value::Undefined, + yielded: JsValue::Undefined, done: false, frame: id, pc: frame.fault_pc, generation: frame.iterator_generation, realm: frame.executable.realm, array: None, - iterable: Value::Undefined, + iterable: JsValue::Undefined, position: 0, stage: Stage::Start, builtin_probe: false, - iterator: Value::Undefined, - next: Value::Undefined, + iterator: JsValue::Undefined, + next: JsValue::Undefined, fast: None, ready: false, abrupt: None, - argument: Value::Undefined, + argument: JsValue::Undefined, sync_fallback: false, }; Ok(pending) @@ -753,12 +807,14 @@ impl PendingIteratorState { } self.stage = Stage::ReturnMethod; return Ok(Action::Read( - self.iterator.clone(), + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, self.key(runtime, crate::engine::atom::pinned::PinnedAtom::Return)?, )); } Some(Completion::Return(value)) => value, - None if matches!(self.stage, Stage::Start) => Value::Undefined, + None if matches!(self.stage, Stage::Start) => JsValue::Undefined, None => return Err(Error::internal("iterator stage lost its reply")), }; match self.stage { @@ -766,7 +822,9 @@ impl PendingIteratorState { Stage::Close => { self.stage = Stage::ReturnMethod; Ok(Action::Read( - self.iterator.clone(), + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, self.key(runtime, crate::engine::atom::pinned::PinnedAtom::Return)?, )) } @@ -786,7 +844,9 @@ impl PendingIteratorState { { self.stage = Stage::AsyncMethod; Ok(Action::Read( - self.iterable.clone(), + runtime + .dup_jsvalue(&self.iterable) + .map_err(runtime_error_to_vm_error)?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::AsyncIterator)), )) } @@ -797,21 +857,31 @@ impl PendingIteratorState { Stage::Method }; Ok(Action::Read( - self.iterable.clone(), + runtime + .dup_jsvalue(&self.iterable) + .map_err(runtime_error_to_vm_error)?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), )) } Stage::Probe => { + let probe = runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?; self.builtin_probe = super::iterator_support::is_direct_native_target( runtime, - &value, + &probe, NativeFunctionId::ArrayPrototypeIterator(ArrayIteratorKind::Value), )?; + drop(probe); // Release the first result before the second observable GetIterator lookup. - drop(value); + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; self.stage = Stage::Method; Ok(Action::Read( - self.iterable.clone(), + runtime + .dup_jsvalue(&self.iterable) + .map_err(runtime_error_to_vm_error)?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), )) } @@ -827,35 +897,47 @@ impl PendingIteratorState { )?; self.stage = Stage::Iterator; let receiver = if matches!(self.mode, Mode::Start { .. }) { - std::mem::replace(&mut self.iterable, Value::Undefined) + std::mem::replace(&mut self.iterable, JsValue::Undefined) } else { - self.iterable.clone() + runtime + .dup_jsvalue(&self.iterable) + .map_err(runtime_error_to_vm_error)? }; Ok(Action::Call(callable, receiver)) } Stage::Iterator => { - if !matches!(value, Value::Object(_)) { + if !matches!(value, JsValue::Object(_)) { return Err(Error::new(ErrorKind::Type, "not an object")); } self.iterator = value; self.stage = Stage::NextMethod; Ok(Action::Read( - self.iterator.clone(), + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, self.key(runtime, crate::engine::atom::pinned::PinnedAtom::Next)?, )) } Stage::NextMethod if self.sync_fallback => { - let Value::Object(iterator) = &self.iterator else { + let JsValue::Object(iterator) = self.iterator else { return Err(Error::internal("async fallback lost its iterator")); }; - self.iterator = Value::Object( - runtime - .new_async_from_sync_iterator(self.realm, iterator, &value) - .map_err(runtime_error_to_vm_error)?, - ); + let wrapper = runtime + .new_async_from_sync_iterator_jsvalue(self.realm, iterator, &value) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(std::mem::replace(&mut self.iterator, JsValue::Undefined)) + .map_err(runtime_error_to_vm_error)?; + let id = wrapper.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + self.iterator = JsValue::Object(id); self.sync_fallback = false; Ok(Action::Read( - self.iterator.clone(), + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, self.key(runtime, crate::engine::atom::pinned::PinnedAtom::Next)?, )) } @@ -864,16 +946,37 @@ impl PendingIteratorState { if matches!(self.mode, Mode::Start { .. }) { return Ok(Action::Finish); } - self.fast = super::iterator_support::append_fast_array_values( + let iterable = runtime + .root_value(&self.iterable) + .map_err(runtime_error_to_vm_error)?; + let next = runtime + .root_value(&self.next) + .map_err(runtime_error_to_vm_error)?; + let fast = super::iterator_support::append_fast_array_values( runtime, - &self.iterable, - &self.next, + &iterable, + &next, self.builtin_probe, - )? - .map(Vec::into_iter); + )?; + drop(iterable); + drop(next); + self.fast = match fast { + Some(values) => { + let mut internal = Vec::with_capacity(values.len()); + for value in values { + internal.push( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ); + } + Some(internal.into_iter()) + } + None => None, + }; self.ready = true; self.stage = Stage::Next; - Ok(Action::Reply(Completion::Return(Value::Undefined))) + Ok(Action::Reply(Completion::Return(JsValue::Undefined))) } Stage::Next => { if let Some(values) = self.fast.as_mut() { @@ -883,8 +986,19 @@ impl PendingIteratorState { self.stage = Stage::Value; return Ok(Action::Reply(Completion::Return(value))); } - let next = callable(runtime, self.next.clone(), "not a function")?; - Ok(Action::Next(next, self.iterator.clone())) + let next = callable( + runtime, + runtime + .dup_jsvalue(&self.next) + .map_err(runtime_error_to_vm_error)?, + "not a function", + )?; + Ok(Action::Next( + next, + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, + )) } Stage::Value => { @@ -896,6 +1010,9 @@ impl PendingIteratorState { let key = runtime .property_key_for_index(self.position as u64) .map_err(|e| Error::internal(e.to_string()))?; + let value_root = runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?; let outcome = runtime .define_own_property_in_realm( Some(self.realm), @@ -904,7 +1021,7 @@ impl PendingIteratorState { .ok_or_else(|| Error::internal("Append lost its target"))?, &key, &OrdinaryPropertyDescriptor { - value: DescriptorField::Present(value), + value: DescriptorField::Present(value_root), writable: DescriptorField::Present(true), enumerable: DescriptorField::Present(true), configurable: DescriptorField::Present(true), @@ -912,30 +1029,41 @@ impl PendingIteratorState { }, ) .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; match outcome { PropertyDefineOutcome::Defined(true) => {} PropertyDefineOutcome::Defined(false) => { return Err(Error::new(ErrorKind::Type, "property is not configurable")); } - PropertyDefineOutcome::Throw(value) => { - return Ok(Action::Reply(Completion::Throw(value))); + PropertyDefineOutcome::Throw(thrown) => { + let thrown = runtime + .into_jsvalue(thrown) + .map_err(runtime_error_to_vm_error)?; + return Ok(Action::Reply(Completion::Throw(thrown))); } } self.position = self.position.wrapping_add(1); self.stage = Stage::Next; - Ok(Action::Reply(Completion::Return(Value::Undefined))) + Ok(Action::Reply(Completion::Return(JsValue::Undefined))) } Stage::ReturnMethod => { - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { return Ok(Action::Finish); } let method = callable(runtime, value, "not a function")?; self.stage = Stage::ReturnResult; - Ok(Action::Call(method, self.iterator.clone())) + Ok(Action::Call( + method, + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, + )) } // With an exception pending, even a primitive return result is ignored. Stage::ReturnResult => { - if self.abrupt.is_none() && !matches!(value, Value::Object(_)) { + if self.abrupt.is_none() && !matches!(value, JsValue::Object(_)) { return Err(Error::new(ErrorKind::Type, "not an object")); } Ok(Action::Finish) @@ -944,16 +1072,19 @@ impl PendingIteratorState { } } -fn callable(runtime: &Runtime, value: Value, message: &str) -> Result { - if let Value::Object(object) = value { - if let Some(callable) = runtime - .as_callable(&object) - .map_err(runtime_error_to_vm_error)? - { - return Ok(callable); - } +fn callable(runtime: &Runtime, value: JsValue, message: &str) -> Result { + let callable = match &value { + JsValue::Object(object) => runtime + .as_callable_object(*object) + .map_err(runtime_error_to_vm_error)?, + _ => None, + }; + let result = callable.ok_or_else(|| Error::new(ErrorKind::Type, message)); + match (result, runtime.release_jsvalue(value)) { + (Ok(callable), Ok(())) => Ok(callable), + (Err(error), _) => Err(error), + (Ok(_), Err(error)) => Err(runtime_error_to_vm_error(error)), } - Err(Error::new(ErrorKind::Type, message)) } #[cfg(all(test, feature = "profiling"))] diff --git a/src/engine/vm/iterator_driver/regions.rs b/src/engine/vm/iterator_driver/regions.rs index 3fd516a7..46d4add5 100644 --- a/src/engine/vm/iterator_driver/regions.rs +++ b/src/engine/vm/iterator_driver/regions.rs @@ -1,9 +1,14 @@ //! Iterator records stay in operand slots; regions hold only validated indices. use super::{CallStep, Completion, Error, FrameId, RunningExecution, Runtime}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::{VmUnwindRegion, frame::Frame, stack::SlotStore}; -pub(super) fn disable(frame: &mut Frame, slots: &mut SlotStore, base: usize) -> Result<(), Error> { +pub(super) fn disable( + runtime: &Runtime, + frame: &mut Frame, + slots: &mut SlotStore, + base: usize, +) -> Result<(), Error> { let body = &mut *frame.cold; let Some(VmUnwindRegion::Iterator { record_base, @@ -22,16 +27,20 @@ pub(super) fn disable(frame: &mut Frame, slots: &mut SlotStore, base: usize) -> .depth(&body.window) .checked_sub(base + 1) .ok_or_else(|| Error::internal("iterator record is truncated"))?; - slots.replace_operand(&body.window, offset, Value::Undefined)?; + let old = slots.replace_operand(&body.window, offset, JsValue::Undefined)?; + runtime + .release_jsvalue(old) + .map_err(super::runtime_error_to_vm_error)?; *enabled = false; Ok(()) } pub(super) fn take( + runtime: &Runtime, frame: &mut Frame, slots: &mut SlotStore, preserve: bool, -) -> Result<(Value, bool, bool), Error> { +) -> Result<(JsValue, bool, bool), Error> { let Some(VmUnwindRegion::Iterator { record_base, enabled, @@ -51,14 +60,22 @@ pub(super) fn take( "iterator cleanup did not reach its record/preserved value", )); } - let iterator = slots.peek(&frame.window, depth - record_base - 1)?.clone(); + let iterator = { + let peeked = slots.peek(&frame.window, depth - record_base - 1)?; + runtime + .dup_jsvalue(peeked) + .map_err(super::runtime_error_to_vm_error)? + }; let value = if preserve { Some(slots.pop(&mut frame.window)?) } else { None }; while slots.depth(&frame.window) > record_base { - slots.pop(&mut frame.window)?; + let discarded = slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(super::runtime_error_to_vm_error)?; } if let Some(value) = value { slots.push(&mut frame.window, value)?; @@ -72,10 +89,10 @@ pub(in crate::engine::vm) fn unwind( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - mut value: Value, + mut value: JsValue, ) -> Result { runtime - .ensure_error_backtrace(&value, false, None) + .ensure_error_backtrace_jsvalue(&value, false, None) .map_err(super::runtime_error_to_vm_error)?; loop { let frame = execution.frames.current_mut(id)?; @@ -94,7 +111,10 @@ pub(in crate::engine::vm) fn unwind( )); } while execution.slots.depth(&frame.window) > stack_depth { - execution.slots.pop(&mut frame.window)?; + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(super::runtime_error_to_vm_error)?; } execution.slots.push(&mut frame.window, value)?; frame.cold.regions.pop(); @@ -115,12 +135,19 @@ pub(in crate::engine::vm) fn unwind( "iterator unwind region exceeds the VM stack", )); } - let iterator = execution - .slots - .peek(&frame.window, depth - record_base - 1)? - .clone(); + let iterator = { + let peeked = execution + .slots + .peek(&frame.window, depth - record_base - 1)?; + runtime + .dup_jsvalue(peeked) + .map_err(super::runtime_error_to_vm_error)? + }; while execution.slots.depth(&frame.window) > record_base { - execution.slots.pop(&mut frame.window)?; + let discarded = execution.slots.pop(&mut frame.window)?; + runtime + .release_jsvalue(discarded) + .map_err(super::runtime_error_to_vm_error)?; } frame.cold.regions.pop(); if enabled { diff --git a/src/engine/vm/iterator_driver/suspension.rs b/src/engine/vm/iterator_driver/suspension.rs index 22445ac7..2a32c6a2 100644 --- a/src/engine/vm/iterator_driver/suspension.rs +++ b/src/engine/vm/iterator_driver/suspension.rs @@ -6,7 +6,7 @@ use super::{ use crate::engine::api::ErrorKind; use crate::engine::code::bytecode::IteratorCallKind; use crate::engine::object::{PropertyKey, WellKnownSymbol}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::{VmUnwindRegion, frame::Frame}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -45,8 +45,12 @@ pub(super) fn start( return drive(runtime, execution, pending, None); } Operation::Next | Operation::Call(_) => { - let iterator = execution.slots.peek(&frame.window, 3)?.clone(); - let next = execution.slots.peek(&frame.window, 2)?.clone(); + let iterator = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 3)?) + .map_err(runtime_error_to_vm_error)?; + let next = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 2)?) + .map_err(runtime_error_to_vm_error)?; let mode = if let Operation::Call(kind) = op { Mode::Delegate(kind) } else { @@ -58,7 +62,9 @@ pub(super) fn start( pending.argument = if matches!(op, Operation::Next) { execution.slots.pop(&mut frame.window)? } else { - execution.slots.peek(&frame.window, 0)?.clone() + runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)? }; pending } @@ -90,7 +96,7 @@ pub(super) fn start( let mut pending = PendingIteratorState::new(frame, id, mode)?; if matches!(op, Operation::Parse) { pending.iterator = execution.slots.pop(&mut frame.window)?; - if !matches!(pending.iterator, Value::Object(_)) { + if !matches!(pending.iterator, JsValue::Object(_)) { pending.stage = Stage::Finish; let error = super::materialize( runtime, @@ -100,8 +106,12 @@ pub(super) fn start( return drive(runtime, execution, pending, Some(error)); } } else { - pending.iterator = execution.slots.peek(&frame.window, 1)?.clone(); - pending.next = execution.slots.peek(&frame.window, 0)?.clone(); + pending.iterator = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 1)?) + .map_err(runtime_error_to_vm_error)?; + pending.next = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?; let Some(VmUnwindRegion::Iterator { enabled, .. }) = frame.cold.regions.last_mut() else { unreachable!() @@ -114,8 +124,11 @@ pub(super) fn start( let action = match op { Operation::Next | Operation::AwaitNext => { pending.stage = Stage::ResumeResult; + let next = runtime + .dup_jsvalue(&pending.next) + .map_err(runtime_error_to_vm_error)?; let target = match runtime - .direct_call_target_from_value(pending.next.clone()) + .direct_call_target_from_jsvalue(next) .map_err(runtime_error_to_vm_error) { Ok(target) => target, @@ -125,11 +138,13 @@ pub(super) fn start( } }; let arguments = if matches!(op, Operation::Next) { - vec![std::mem::replace(&mut pending.argument, Value::Undefined)] + vec![std::mem::replace(&mut pending.argument, JsValue::Undefined)] } else { Vec::new() }; - let receiver = pending.iterator.clone(); + let receiver = runtime + .dup_jsvalue(&pending.iterator) + .map_err(runtime_error_to_vm_error)?; return crate::engine::vm::proxy_get_driver::start_iterator_invoke( runtime, execution, @@ -154,7 +169,9 @@ pub(super) fn start( } Operation::Start { .. } => unreachable!(), }; - let receiver = pending.iterator.clone(); + let receiver = runtime + .dup_jsvalue(&pending.iterator) + .map_err(runtime_error_to_vm_error)?; crate::engine::vm::proxy_get_driver::start_iterator_read( runtime, execution, @@ -184,15 +201,17 @@ impl PendingIteratorState { pub(super) fn advance_suspension( &mut self, runtime: &Runtime, - value: Value, + value: JsValue, ) -> Result { match self.stage { Stage::AsyncMethod => { self.stage = Stage::Method; - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { self.sync_fallback = true; Ok(Action::Read( - self.iterable.clone(), + runtime + .dup_jsvalue(&self.iterable) + .map_err(runtime_error_to_vm_error)?, PropertyKey::from(runtime.well_known_symbol(WellKnownSymbol::Iterator)), )) } else { @@ -200,12 +219,12 @@ impl PendingIteratorState { self.stage = Stage::Iterator; Ok(Action::Call( method, - std::mem::replace(&mut self.iterable, Value::Undefined), + std::mem::replace(&mut self.iterable, JsValue::Undefined), )) } } Stage::DelegateMethod => { - if matches!(value, Value::Undefined | Value::Null) { + if matches!(value, JsValue::Undefined | JsValue::Null) { self.done = true; return Ok(Action::Finish); } @@ -214,14 +233,20 @@ impl PendingIteratorState { }; self.stage = Stage::ResumeResult; let target = runtime - .direct_call_target_from_value(value) + .direct_call_target_from_jsvalue(value) .map_err(runtime_error_to_vm_error)?; let arguments = if kind == IteratorCallKind::ReturnWithoutValue { Vec::new() } else { - vec![std::mem::replace(&mut self.argument, Value::Undefined)] + vec![std::mem::replace(&mut self.argument, JsValue::Undefined)] }; - Ok(Action::Invoke(target, self.iterator.clone(), arguments)) + Ok(Action::Invoke( + target, + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, + arguments, + )) } Stage::ResumeResult | Stage::ValueProperty => { self.yielded = value; @@ -229,12 +254,14 @@ impl PendingIteratorState { } Stage::DoneProperty => { self.done = runtime - .value_to_boolean(&value) + .value_to_boolean_jsvalue(&value) .map_err(runtime_error_to_vm_error)?; self.stage = Stage::ValueProperty; // Unlike sync ForOfNext, value is read even when done is true. Ok(Action::Read( - self.iterator.clone(), + runtime + .dup_jsvalue(&self.iterator) + .map_err(runtime_error_to_vm_error)?, self.key(runtime, crate::engine::atom::pinned::PinnedAtom::Value)?, )) } diff --git a/src/engine/vm/method_arguments.rs b/src/engine/vm/method_arguments.rs index 8de7573f..97598a46 100644 --- a/src/engine/vm/method_arguments.rs +++ b/src/engine/vm/method_arguments.rs @@ -2,8 +2,9 @@ use super::bindings::FrameBinding; use super::stack::{RunSlots, copy_value}; use crate::engine::api::error::Error; +use crate::engine::api::runtime::Runtime; use crate::engine::code::bytecode::Instruction; -use crate::engine::value::Value; +use crate::engine::value::JsValue; pub(super) fn available(slots: &RunSlots<'_>, instructions: &[Instruction]) -> bool { instructions.iter().all(|instruction| match instruction { @@ -17,25 +18,29 @@ pub(super) fn available(slots: &RunSlots<'_>, instructions: &[Instruction]) -> b }) } -pub(super) fn argument(slots: &RunSlots<'_>, instruction: &Instruction) -> Result { +pub(super) fn argument( + runtime: &Runtime, + slots: &RunSlots<'_>, + instruction: &Instruction, +) -> Result { Ok(match instruction { Instruction::GetLocal(index) | Instruction::GetLocalCheck(index) => { let FrameBinding::Direct(value) = slots.local(*index)? else { unreachable!("preflighted direct method argument") }; - copy_value(value)? + copy_value(runtime, value)? } Instruction::GetArg(index) => { let FrameBinding::Direct(value) = slots.parameter(*index)? else { unreachable!("preflighted direct method parameter") }; - copy_value(value)? + copy_value(runtime, value)? } - Instruction::PushI32(value) => Value::Int(*value), - Instruction::Undefined => Value::Undefined, - Instruction::Null => Value::Null, - Instruction::PushTrue => Value::Bool(true), - Instruction::PushFalse => Value::Bool(false), + Instruction::PushI32(value) => JsValue::Int(*value), + Instruction::Undefined => JsValue::Undefined, + Instruction::Null => JsValue::Null, + Instruction::PushTrue => JsValue::Bool(true), + Instruction::PushFalse => JsValue::Bool(false), _ => unreachable!("published method call span"), }) } diff --git a/src/engine/vm/mod.rs b/src/engine/vm/mod.rs index ade7fe2a..339abb7d 100644 --- a/src/engine/vm/mod.rs +++ b/src/engine/vm/mod.rs @@ -68,6 +68,7 @@ pub(crate) use completion::{ }; mod numeric; +pub(crate) use numeric::to_js_string_jsvalue; mod activation; pub use activation::VmUnwindRegion; diff --git a/src/engine/vm/native_stack.rs b/src/engine/vm/native_stack.rs index a60ba645..9610ada2 100644 --- a/src/engine/vm/native_stack.rs +++ b/src/engine/vm/native_stack.rs @@ -161,7 +161,7 @@ impl Runtime { if function_kind == FunctionKind::Async { return self.reject_async_bytecode_stack_overflow(caller_realm); } - Ok(Completion::Throw(self.new_native_error( + Ok(Completion::Throw(self.new_native_error_jsvalue( caller_realm, NativeErrorKind::Internal, "stack overflow", diff --git a/src/engine/vm/numeric.rs b/src/engine/vm/numeric.rs index 0e70f972..33064add 100644 --- a/src/engine/vm/numeric.rs +++ b/src/engine/vm/numeric.rs @@ -1,8 +1,10 @@ pub(super) mod operation; use crate::engine::{ + api::runtime::Runtime, api::{Error, ErrorKind}, + heap::{BigIntId, StringId}, value::{ - Value, + JsString, JsValue, bigint::{BigIntError, JsBigInt}, }, }; @@ -14,21 +16,180 @@ pub(in crate::engine::vm) enum NumericValue { BigInt(JsBigInt), } -pub(in crate::engine::vm) fn to_numeric_primitive(value: Value) -> Result { +/// Read one string node's payload. The borrowed value keeps its edge. +pub(in crate::engine::vm) fn string_payload( + runtime: &Runtime, + id: StringId, +) -> Result { + Ok(runtime + .0 + .state + .borrow() + .heap + .string(id) + .map_err(|error| Error::internal(error.to_string()))? + .clone()) +} + +/// Read one BigInt node's payload. The borrowed value keeps its edge. +pub(in crate::engine::vm) fn bigint_payload( + runtime: &Runtime, + id: BigIntId, +) -> Result { + Ok(runtime + .0 + .state + .borrow() + .heap + .bigint(id) + .map_err(|error| Error::internal(error.to_string()))? + .clone()) +} + +/// Publish a freshly produced string payload as an owned internal value. +/// Concatenation and primitive formatting are genuine string creation points. +pub(in crate::engine::vm) fn allocate_string_jsvalue( + runtime: &Runtime, + string: JsString, +) -> Result { + let id = runtime + .0 + .state + .borrow_mut() + .heap + .allocate_string(string) + .map_err(|error| Error::internal(error.to_string()))?; + Ok(JsValue::String(id)) +} + +/// Publish a freshly produced BigInt payload as an owned internal value. +/// BigInt arithmetic results are genuine BigInt creation points. +pub(in crate::engine::vm) fn allocate_bigint_jsvalue( + runtime: &Runtime, + bigint: JsBigInt, +) -> Result { + let id = runtime + .0 + .state + .borrow_mut() + .heap + .allocate_bigint(bigint) + .map_err(|error| Error::internal(error.to_string()))?; + #[cfg(debug_assertions)] + if std::env::var("QJS_TRACE_BIGINT_ID") + .is_ok_and(|value| format!("{id:?}").contains(&format!("index: {value},"))) + { + eprintln!( + "[alloc-b] {id:?}\n{}", + std::backtrace::Backtrace::force_capture() + ); + } + Ok(JsValue::BigInt(id)) +} + +/// Representation-only `ToNumber` for internal values. Object conversion must +/// be routed through a context; Symbol and BigInt conversion throw here. +pub(in crate::engine::vm) fn to_number_jsvalue( + runtime: &Runtime, + value: &JsValue, +) -> Result { + Ok(match value { + JsValue::Undefined => f64::NAN, + JsValue::Null => 0.0, + JsValue::Bool(value) => { + if *value { + 1.0 + } else { + 0.0 + } + } + JsValue::Int(value) => f64::from(*value), + JsValue::Float(value) => *value, + JsValue::String(id) => { + crate::engine::value::string_to_number(&string_payload(runtime, *id)?) + } + JsValue::BigInt(_) => { + return Err(Error::new( + ErrorKind::Type, + "cannot convert bigint to number", + )); + } + JsValue::Symbol(_) => { + return Err(Error::new( + ErrorKind::Type, + "cannot convert symbol to number", + )); + } + JsValue::Object(_) => { + return Err(Error::internal( + "object ToNumber requires an execution context", + )); + } + }) +} + +/// Primitive `ToString` payload for internal values (no object conversion). +pub(crate) fn to_js_string_jsvalue(runtime: &Runtime, value: &JsValue) -> Result { + Ok(match value { + JsValue::String(id) => string_payload(runtime, *id)?, + JsValue::Undefined => JsString::from_static("undefined"), + JsValue::Null => JsString::from_static("null"), + JsValue::Bool(true) => JsString::from_static("true"), + JsValue::Bool(false) => JsString::from_static("false"), + JsValue::Int(value) => JsString::from_owned_latin1(value.to_string().into_bytes()), + JsValue::Float(value) => { + JsString::from_owned_latin1(crate::engine::value::number_to_string(*value).into_bytes()) + } + JsValue::BigInt(id) => { + let bigint = bigint_payload(runtime, *id)?; + if bigint.exceeds_allocation_limit() { + return Err(Error::new( + ErrorKind::Range, + "BigInt is too large to allocate", + )); + } + JsString::from_owned_latin1(bigint.to_string().into_bytes()) + } + JsValue::Symbol(_) => { + return Err(Error::new( + ErrorKind::Type, + "cannot convert symbol to string", + )); + } + JsValue::Object(_) => { + return Err(Error::internal( + "object ToPrimitive requires an execution context", + )); + } + }) +} + +pub(in crate::engine::vm) fn to_numeric_primitive( + runtime: &Runtime, + value: &JsValue, +) -> Result { match value { - Value::BigInt(value) => Ok(NumericValue::BigInt(value)), - value => Ok(NumericValue::Number(value.to_number()?)), + JsValue::BigInt(id) => Ok(NumericValue::BigInt(bigint_payload(runtime, *id)?)), + value => Ok(NumericValue::Number(to_number_jsvalue(runtime, value)?)), } } /// OP_plus after ToPrimitive: preserve numeric tags and its specific BigInt /// diagnostic. This step cannot invoke user code. -pub(in crate::engine::vm) fn unary_plus_primitive(value: Value) -> Result { - match value { - Value::BigInt(_) => Err(Error::new(ErrorKind::Type, "bigint argument with unary +")), - value @ (Value::Int(_) | Value::Float(_)) => Ok(value), - value => Ok(Value::number(value.to_number()?)), +pub(in crate::engine::vm) fn unary_plus_primitive( + runtime: &Runtime, + value: JsValue, +) -> Result { + if matches!(value, JsValue::BigInt(_)) { + release_primitive_operand(runtime, value)?; + return Err(Error::new(ErrorKind::Type, "bigint argument with unary +")); } + if matches!(value, JsValue::Int(_) | JsValue::Float(_)) { + return Ok(value); + } + let result = jsvalue_number(to_number_jsvalue(runtime, &value)?); + release_primitive_operand(runtime, value)?; + Ok(result) } /// ECMAScript `ToInt32`, matching QuickJS's modulo-2^32 conversion for every @@ -41,6 +202,23 @@ pub(in crate::engine::vm) fn number_to_uint32(value: f64) -> u32 { u32::from_ne_bytes(number_to_int32(value).to_ne_bytes()) } +/// Compact a numeric payload into the internal number representation. +pub(in crate::engine::vm) fn jsvalue_number(value: f64) -> JsValue { + jsvalue_from_number(crate::engine::value::number::operations::Number::compact( + value, + )) +} + +/// Project an already-compacted numeric representation without recompacting. +pub(in crate::engine::vm) fn jsvalue_from_number( + number: crate::engine::value::number::operations::Number, +) -> JsValue { + match number { + crate::engine::value::number::operations::Number::Int(value) => JsValue::Int(value), + crate::engine::value::number::operations::Number::Float(value) => JsValue::Float(value), + } +} + pub(in crate::engine::vm) fn compare_bigint_number( bigint: &JsBigInt, number: f64, @@ -89,55 +267,71 @@ pub(in crate::engine::vm) fn bigint_error(error: BigIntError) -> Error { } /// Addition after both operands have completed ToPrimitive, in order. -pub(in crate::engine::vm) fn add_primitives(left: Value, right: Value) -> Result { - if matches!(left, Value::String(_)) || matches!(right, Value::String(_)) { - let left = match left { - Value::String(value) => value, - value => value.to_js_string()?, +pub(in crate::engine::vm) fn add_primitives( + runtime: &Runtime, + left: JsValue, + right: JsValue, +) -> Result { + let result = if matches!(left, JsValue::String(_)) || matches!(right, JsValue::String(_)) { + let left = match &left { + JsValue::String(id) => string_payload(runtime, *id)?, + value => to_js_string_jsvalue(runtime, value)?, }; - let right = match right { - Value::String(value) => value, - value => value.to_js_string()?, + let right = match &right { + JsValue::String(id) => string_payload(runtime, *id)?, + value => to_js_string_jsvalue(runtime, value)?, }; - return Ok(Value::String(left.concat_owned(&right)?)); - } - add_primitives_ref(&left, &right) + allocate_string_jsvalue(runtime, left.concat_owned(&right)?) + } else { + add_primitives_ref(runtime, &left, &right) + }; + release_primitive_operand(runtime, left)?; + release_primitive_operand(runtime, right)?; + result +} + +fn release_primitive_operand(runtime: &Runtime, value: JsValue) -> Result<(), Error> { + runtime + .release_jsvalue(value) + .map_err(|error| Error::internal(error.to_string())) } /// Same primitive kernel with owners retained by the caller. No user code can /// execute; callers may borrow frame locals without cloning temporary roots. pub(in crate::engine::vm) fn add_primitives_ref( - left: &Value, - right: &Value, -) -> Result { - if matches!(left, Value::String(_)) || matches!(right, Value::String(_)) { - use std::borrow::Cow; + runtime: &Runtime, + left: &JsValue, + right: &JsValue, +) -> Result { + if matches!(left, JsValue::String(_)) || matches!(right, JsValue::String(_)) { let left = match left { - Value::String(value) => Cow::Borrowed(value), - value => Cow::Owned(value.to_js_string()?), + JsValue::String(id) => string_payload(runtime, *id)?, + value => to_js_string_jsvalue(runtime, value)?, }; let right = match right { - Value::String(value) => Cow::Borrowed(value), - value => Cow::Owned(value.to_js_string()?), + JsValue::String(id) => string_payload(runtime, *id)?, + value => to_js_string_jsvalue(runtime, value)?, }; - return Ok(Value::String(left.try_concat(&right).map_err(Error::from)?)); + return allocate_string_jsvalue(runtime, left.try_concat(&right).map_err(Error::from)?); } match (left, right) { - (Value::BigInt(left), Value::BigInt(right)) => { - Ok(Value::BigInt(left.add(right).map_err(bigint_error)?)) + (JsValue::BigInt(left), JsValue::BigInt(right)) => { + let left = bigint_payload(runtime, *left)?; + let right = bigint_payload(runtime, *right)?; + allocate_bigint_jsvalue(runtime, left.add(&right).map_err(bigint_error)?) } - (Value::BigInt(_), right) => { - right.to_number()?; + (JsValue::BigInt(_), right) => { + to_number_jsvalue(runtime, right)?; Err(mixed_numeric_type_error()) } - (left, Value::BigInt(_)) => { - left.to_number()?; + (left, JsValue::BigInt(_)) => { + to_number_jsvalue(runtime, left)?; Err(mixed_numeric_type_error()) } (left, right) => { - let left = left.to_number()?; - let right = right.to_number()?; - Ok(Value::number(left + right)) + let left = to_number_jsvalue(runtime, left)?; + let right = to_number_jsvalue(runtime, right)?; + Ok(jsvalue_number(left + right)) } } } diff --git a/src/engine/vm/numeric/operation.rs b/src/engine/vm/numeric/operation.rs index 6bb5a304..8f49eba4 100644 --- a/src/engine/vm/numeric/operation.rs +++ b/src/engine/vm/numeric/operation.rs @@ -1,13 +1,14 @@ //! Numeric operators retain ordered conversion operands across JavaScript callbacks. use super::{ - NumericValue, add_primitives, bigint_error, compare_bigint_number, mixed_numeric_type_error, - number_to_int32, number_to_uint32, string_to_bigint, to_numeric_primitive, - unary_plus_primitive, + NumericValue, add_primitives, bigint_error, bigint_payload, compare_bigint_number, + jsvalue_number, mixed_numeric_type_error, number_to_int32, number_to_uint32, string_payload, + string_to_bigint, to_number_jsvalue, to_numeric_primitive, unary_plus_primitive, }; use crate::engine::{ + api::runtime::Runtime, api::{Error, ErrorKind}, code::bytecode::Instruction, - value::{Value, bigint::JsBigInt}, + value::JsValue, vm::{Completion, ToPrimitiveHint}, }; @@ -90,11 +91,11 @@ impl NumericKind { } } pub(in crate::engine::vm) struct NumericOutput { - pub value: Value, - pub previous: Option, + pub value: JsValue, + pub previous: Option, } impl NumericOutput { - fn value(value: Value) -> Self { + fn value(value: JsValue) -> Self { Self { value, previous: None, @@ -112,35 +113,38 @@ impl NumericOutput { /// Parsing, allocation and final primitive-owner release require an ended /// RunSlots borrow; the resident run helper is also such an owning boundary. pub(in crate::engine::vm) fn primitive_output( + runtime: &Runtime, kind: NumericKind, - left: Value, - right: Option, + left: JsValue, + right: Option, ) -> Result { if kind.unary() { - return unary_output(kind, left); + return unary_output(runtime, kind, left); } let right = right.ok_or_else(|| Error::internal("binary numeric operator lost RHS"))?; if kind == NumericKind::Add { - return add_primitives(left, right).map(NumericOutput::value); + return add_primitives(runtime, left, right).map(NumericOutput::value); } - let left = to_numeric_primitive(left)?; - let right = to_numeric_primitive(right)?; - binary(kind, left, right).map(NumericOutput::value) + let converted_left = to_numeric_primitive(runtime, &left); + let converted_right = to_numeric_primitive(runtime, &right); + super::release_primitive_operand(runtime, left)?; + super::release_primitive_operand(runtime, right)?; + binary(runtime, kind, converted_left?, converted_right?).map(NumericOutput::value) } pub(in crate::engine::vm) enum NumericStep { Complete { - value: Value, - previous: Option, + value: JsValue, + previous: Option, }, - Throw(Value), + Throw(JsValue), Primitive { - value: Value, + value: JsValue, hint: ToPrimitiveHint, resume: NumericResume, }, HtmlDda { - value: Value, + value: JsValue, resume: NumericResume, }, } @@ -163,21 +167,23 @@ pub(in crate::engine::vm) struct NumericResumeState { } enum Phase { Unary, - Left(Value), - RightPrimitive(Value), + Left(JsValue), + RightPrimitive(JsValue), RightNumeric(NumericValue), - EqualityLeft(Value), - EqualityRight(Value), - EqualityDda(Value, Value), + EqualityLeft(JsValue), + EqualityRight(JsValue), + EqualityDda(JsValue, JsValue), } impl NumericStep { pub(in crate::engine::vm) fn start( + runtime: &Runtime, kind: NumericKind, - left: Value, - right: Option, + left: JsValue, + right: Option, ) -> Result { if kind.unary() { return primitive( + runtime, left, ToPrimitiveHint::Number, NumericResume(Box::new(NumericResumeState { @@ -188,9 +194,10 @@ impl NumericStep { } let right = right.ok_or_else(|| Error::internal("binary numeric operator lost RHS"))?; if matches!(kind, NumericKind::Eq | NumericKind::Neq) { - return equality(kind, left, right, false); + return equality(runtime, kind, left, right, false); } primitive( + runtime, left, if kind == NumericKind::Add { ToPrimitiveHint::Default @@ -205,139 +212,198 @@ impl NumericStep { } } fn primitive( - value: Value, + runtime: &Runtime, + value: JsValue, hint: ToPrimitiveHint, resume: NumericResume, ) -> Result { - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { Ok(NumericStep::Primitive { value, hint, resume, }) } else { - resume.resume(Completion::Return(value)) + resume.resume(runtime, Completion::Return(value)) } } -fn complete(value: Value) -> NumericStep { +fn complete(value: JsValue) -> NumericStep { NumericStep::Complete { value, previous: None, } } impl NumericResume { - pub(in crate::engine::vm) fn resume(self, reply: Completion) -> Result { + pub(in crate::engine::vm) fn resume( + self, + runtime: &Runtime, + reply: Completion, + ) -> Result { let value = match reply { Completion::Return(value) => value, Completion::Throw(value) => return Ok(NumericStep::Throw(value)), }; - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { return Err(Error::internal( "numeric ToPrimitive reply returned an object", )); } let kind = self.0.kind; match self.0.phase { - Phase::Unary => unary(kind, value), + Phase::Unary => unary(runtime, kind, value), Phase::Left(right) => { // Arithmetic converts the left primitive to Numeric before starting // the right callback. Relational comparison converts both primitives first. - let phase = if kind == NumericKind::Add || kind.comparison() { - Phase::RightPrimitive(value) + let hint = if kind == NumericKind::Add { + ToPrimitiveHint::Default } else { - Phase::RightNumeric(to_numeric_primitive(value)?) + ToPrimitiveHint::Number }; + if kind == NumericKind::Add || kind.comparison() { + return primitive( + runtime, + right, + hint, + NumericResume(Box::new(NumericResumeState { + kind, + phase: Phase::RightPrimitive(value), + })), + ); + } + let converted = to_numeric_primitive(runtime, &value); + super::release_primitive_operand(runtime, value)?; primitive( + runtime, right, - if kind == NumericKind::Add { - ToPrimitiveHint::Default - } else { - ToPrimitiveHint::Number - }, - NumericResume(Box::new(NumericResumeState { kind, phase })), + hint, + NumericResume(Box::new(NumericResumeState { + kind, + phase: Phase::RightNumeric(converted?), + })), ) } - Phase::RightPrimitive(left) => Ok(complete(if kind == NumericKind::Add { - add_primitives(left, value)? - } else { - Value::Bool(compare(kind, left, value)?) - })), + Phase::RightPrimitive(left) => { + if kind == NumericKind::Add { + return Ok(complete(add_primitives(runtime, left, value)?)); + } + let compared = compare(runtime, kind, &left, &value); + super::release_primitive_operand(runtime, left)?; + super::release_primitive_operand(runtime, value)?; + Ok(complete(JsValue::Bool(compared?))) + } Phase::RightNumeric(left) => { - Ok(complete(binary(kind, left, to_numeric_primitive(value)?)?)) + let converted = to_numeric_primitive(runtime, &value); + super::release_primitive_operand(runtime, value)?; + Ok(complete(binary(runtime, kind, left, converted?)?)) } - Phase::EqualityLeft(right) => equality(kind, value, right, false), - Phase::EqualityRight(left) => equality(kind, left, value, false), + Phase::EqualityLeft(right) => equality(runtime, kind, value, right, false), + Phase::EqualityRight(left) => equality(runtime, kind, left, value, false), Phase::EqualityDda(..) => { Err(Error::internal("HTMLDDA check received a primitive reply")) } } } - pub(in crate::engine::vm) fn html_dda(self, value: bool) -> Result { + pub(in crate::engine::vm) fn html_dda( + self, + runtime: &Runtime, + value: bool, + ) -> Result { let Phase::EqualityDda(left, right) = self.0.phase else { return Err(Error::internal("HTMLDDA reply lost equality owner")); }; if value { Ok(equal_result(self.0.kind, true)) } else { - equality(self.0.kind, left, right, true) + equality(runtime, self.0.kind, left, right, true) } } } -fn unary(kind: NumericKind, value: Value) -> Result { - unary_output(kind, value).map(NumericOutput::into_step) +fn unary(runtime: &Runtime, kind: NumericKind, value: JsValue) -> Result { + unary_output(runtime, kind, value).map(NumericOutput::into_step) } -fn unary_output(kind: NumericKind, value: Value) -> Result { +fn unary_output( + runtime: &Runtime, + kind: NumericKind, + value: JsValue, +) -> Result { if kind == NumericKind::Plus { - return Ok(NumericOutput::value(unary_plus_primitive(value)?)); + return Ok(NumericOutput::value(unary_plus_primitive(runtime, value)?)); } if kind == NumericKind::Neg { - return Ok(NumericOutput::value( - if let Some(number) = value.as_number_repr() { - number.negate().into() - } else { - match value { - Value::BigInt(value) => Value::BigInt(value.neg().map_err(bigint_error)?), - value => Value::number(-value.to_number()?), + if let Some(number) = value.as_number_repr() { + return Ok(NumericOutput::value(match number.negate() { + crate::engine::value::number::operations::Number::Int(value) => JsValue::Int(value), + crate::engine::value::number::operations::Number::Float(value) => { + JsValue::Float(value) } - }, - )); + })); + } + let result = match &value { + JsValue::BigInt(id) => super::allocate_bigint_jsvalue( + runtime, + bigint_payload(runtime, *id)?.neg().map_err(bigint_error)?, + ), + other => Ok(jsvalue_number(-to_number_jsvalue(runtime, other)?)), + }; + super::release_primitive_operand(runtime, value)?; + return result.map(NumericOutput::value); } if kind == NumericKind::BitNot { - return Ok(NumericOutput::value(match to_numeric_primitive(value)? { - NumericValue::BigInt(value) => Value::BigInt(value.bit_not().map_err(bigint_error)?), - NumericValue::Number(value) => Value::Int(!number_to_int32(value)), - })); + let result = match to_numeric_primitive(runtime, &value)? { + NumericValue::BigInt(value) => { + super::allocate_bigint_jsvalue(runtime, value.bit_not().map_err(bigint_error)?) + } + NumericValue::Number(value) => Ok(JsValue::Int(!number_to_int32(value))), + }; + super::release_primitive_operand(runtime, value)?; + return result.map(NumericOutput::value); } let increment = matches!(kind, NumericKind::Inc | NumericKind::PostInc); let postfix = matches!(kind, NumericKind::PostInc | NumericKind::PostDec); - let (old, next) = if let Some(number) = value.as_number_repr() { - (value, number.update(increment).into()) - } else { - match value { - Value::BigInt(old) => { - let next = if increment { - old.add(&JsBigInt::from(1_i32)) - } else { - old.update_decrement() - } - .map_err(bigint_error)?; - (Value::BigInt(old), Value::BigInt(next)) - } - value => { - let old = value.to_number()?; - ( - Value::number(old), - Value::number(if increment { old + 1.0 } else { old - 1.0 }), - ) + if let Some(number) = value.as_number_repr() { + return Ok(NumericOutput { + value: super::jsvalue_from_number(number.update(increment)), + previous: postfix.then_some(value), + }); + } + match value { + JsValue::BigInt(id) => { + let payload = bigint_payload(runtime, id)?; + let next = if increment { + payload.add(&crate::engine::value::bigint::JsBigInt::from(1_i32)) + } else { + payload.update_decrement() } + .map_err(bigint_error)?; + let next = super::allocate_bigint_jsvalue(runtime, next)?; + let old = JsValue::BigInt(id); + let previous = if postfix { + Some(old) + } else { + super::release_primitive_operand(runtime, old)?; + None + }; + Ok(NumericOutput { + value: next, + previous, + }) } - }; - Ok(NumericOutput { - value: next, - previous: postfix.then_some(old), - }) + value => { + let old = to_number_jsvalue(runtime, &value)?; + super::release_primitive_operand(runtime, value)?; + Ok(NumericOutput { + value: jsvalue_number(if increment { old + 1.0 } else { old - 1.0 }), + previous: postfix.then(|| jsvalue_number(old)), + }) + } + } } -fn binary(kind: NumericKind, left: NumericValue, right: NumericValue) -> Result { +fn binary( + runtime: &Runtime, + kind: NumericKind, + left: NumericValue, + right: NumericValue, +) -> Result { if kind == NumericKind::Shr { let (NumericValue::Number(left), NumericValue::Number(right)) = (left, right) else { return Err(Error::new( @@ -345,32 +411,35 @@ fn binary(kind: NumericKind, left: NumericValue, right: NumericValue) -> Result< "bigint operands are forbidden for >>>", )); }; - return Ok(Value::number(f64::from( + return Ok(jsvalue_number(f64::from( number_to_uint32(left) >> (number_to_uint32(right) & 0x1f), ))); } Ok(match (left, right) { - (NumericValue::BigInt(left), NumericValue::BigInt(right)) => Value::BigInt( - match kind { - NumericKind::Sub => left.sub(&right), - NumericKind::Mul => left.mul(&right), - NumericKind::Div => left.div(&right), - NumericKind::Mod => left.rem(&right), - NumericKind::Pow => left.pow(&right), - NumericKind::Shl => left.shl(&right), - NumericKind::Sar => left.shr(&right), - NumericKind::BitAnd => left.bit_and(&right), - NumericKind::BitOr => left.bit_or(&right), - NumericKind::BitXor => left.bit_xor(&right), - _ => { - return Err(Error::internal( - "non-arithmetic operator entered binary Numeric", - )); + (NumericValue::BigInt(left), NumericValue::BigInt(right)) => { + super::allocate_bigint_jsvalue( + runtime, + match kind { + NumericKind::Sub => left.sub(&right), + NumericKind::Mul => left.mul(&right), + NumericKind::Div => left.div(&right), + NumericKind::Mod => left.rem(&right), + NumericKind::Pow => left.pow(&right), + NumericKind::Shl => left.shl(&right), + NumericKind::Sar => left.shr(&right), + NumericKind::BitAnd => left.bit_and(&right), + NumericKind::BitOr => left.bit_or(&right), + NumericKind::BitXor => left.bit_xor(&right), + _ => { + return Err(Error::internal( + "non-arithmetic operator entered binary Numeric", + )); + } } - } - .map_err(bigint_error)?, - ), - (NumericValue::Number(left), NumericValue::Number(right)) => Value::number(match kind { + .map_err(bigint_error)?, + )? + } + (NumericValue::Number(left), NumericValue::Number(right)) => jsvalue_number(match kind { NumericKind::Sub => left - right, NumericKind::Mul => left * right, NumericKind::Div => left / right, @@ -394,19 +463,35 @@ fn binary(kind: NumericKind, left: NumericValue, right: NumericValue) -> Result< _ => return Err(mixed_numeric_type_error()), }) } -fn compare(kind: NumericKind, left: Value, right: Value) -> Result { - let ordering = match (&left, &right) { - (Value::String(left), Value::String(right)) => { +fn compare( + runtime: &Runtime, + kind: NumericKind, + left: &JsValue, + right: &JsValue, +) -> Result { + let ordering = match (left, right) { + (JsValue::String(left), JsValue::String(right)) => { + let left = string_payload(runtime, *left)?; + let right = string_payload(runtime, *right)?; Some(left.utf16_units().cmp(right.utf16_units())) } - (Value::BigInt(left), Value::BigInt(right)) => Some(left.cmp(right)), - (Value::BigInt(left), Value::String(right)) => { - string_to_bigint(right).map(|right| left.cmp(&right)) + (JsValue::BigInt(left), JsValue::BigInt(right)) => { + Some(bigint_payload(runtime, *left)?.cmp(&bigint_payload(runtime, *right)?)) } - (Value::String(left), Value::BigInt(right)) => { - string_to_bigint(left).map(|left| left.cmp(right)) + (JsValue::BigInt(left), JsValue::String(right)) => { + let left = bigint_payload(runtime, *left)?; + let right = string_payload(runtime, *right)?; + string_to_bigint(&right).map(|right| left.cmp(&right)) } - _ => match (to_numeric_primitive(left)?, to_numeric_primitive(right)?) { + (JsValue::String(left), JsValue::BigInt(right)) => { + let left = string_payload(runtime, *left)?; + let right = bigint_payload(runtime, *right)?; + string_to_bigint(&left).map(|left| left.cmp(&right)) + } + _ => match ( + to_numeric_primitive(runtime, left)?, + to_numeric_primitive(runtime, right)?, + ) { (NumericValue::BigInt(left), NumericValue::BigInt(right)) => Some(left.cmp(&right)), (NumericValue::BigInt(left), NumericValue::Number(right)) => { compare_bigint_number(&left, right) @@ -426,31 +511,39 @@ fn compare(kind: NumericKind, left: Value, right: Value) -> Result })) } fn equal_result(kind: NumericKind, equal: bool) -> NumericStep { - complete(Value::Bool(equal != (kind == NumericKind::Neq))) + complete(JsValue::Bool(equal != (kind == NumericKind::Neq))) } fn equality( + runtime: &Runtime, kind: NumericKind, - mut left: Value, - mut right: Value, + mut left: JsValue, + mut right: JsValue, mut checked_dda: bool, ) -> Result { loop { - if left.strict_equal(&right) { + if runtime + .strict_equal_jsvalue(&left, &right) + .map_err(|error| Error::internal(error.to_string()))? + { return Ok(equal_result(kind, true)); } if !checked_dda { - if matches!(right, Value::Null | Value::Undefined) { + if matches!(right, JsValue::Null | JsValue::Undefined) { return Ok(NumericStep::HtmlDda { - value: left.clone(), + value: runtime + .dup_jsvalue(&left) + .map_err(|error| Error::internal(error.to_string()))?, resume: NumericResume(Box::new(NumericResumeState { kind, phase: Phase::EqualityDda(left, right), })), }); } - if matches!(left, Value::Null | Value::Undefined) { + if matches!(left, JsValue::Null | JsValue::Undefined) { return Ok(NumericStep::HtmlDda { - value: right.clone(), + value: runtime + .dup_jsvalue(&right) + .map_err(|error| Error::internal(error.to_string()))?, resume: NumericResume(Box::new(NumericResumeState { kind, phase: Phase::EqualityDda(left, right), @@ -460,50 +553,73 @@ fn equality( } checked_dda = false; match (&left, &right) { - (Value::Null, Value::Undefined) | (Value::Undefined, Value::Null) => { + (JsValue::Null, JsValue::Undefined) | (JsValue::Undefined, JsValue::Null) => { return Ok(equal_result(kind, true)); } - (Value::Int(_) | Value::Float(_), Value::String(_)) => { - right = Value::number(right.to_number()?) + (JsValue::Int(_) | JsValue::Float(_), JsValue::String(_)) => { + let number = to_number_jsvalue(runtime, &right)?; + let old = std::mem::replace(&mut right, jsvalue_number(number)); + runtime + .release_jsvalue(old) + .map_err(|error| Error::internal(error.to_string()))?; } - (Value::String(_), Value::Int(_) | Value::Float(_)) => { - left = Value::number(left.to_number()?) + (JsValue::String(_), JsValue::Int(_) | JsValue::Float(_)) => { + let number = to_number_jsvalue(runtime, &left)?; + let old = std::mem::replace(&mut left, jsvalue_number(number)); + runtime + .release_jsvalue(old) + .map_err(|error| Error::internal(error.to_string()))?; } - (Value::BigInt(a), Value::String(b)) => { + (JsValue::BigInt(a), JsValue::String(b)) => { + let a = bigint_payload(runtime, *a)?; + let b = string_payload(runtime, *b)?; return Ok(equal_result( kind, - string_to_bigint(b).is_some_and(|b| &b == a), + string_to_bigint(&b).is_some_and(|b| b == a), )); } - (Value::String(a), Value::BigInt(b)) => { + (JsValue::String(a), JsValue::BigInt(b)) => { + let a = string_payload(runtime, *a)?; + let b = bigint_payload(runtime, *b)?; return Ok(equal_result( kind, - string_to_bigint(a).is_some_and(|a| &a == b), + string_to_bigint(&a).is_some_and(|a| a == b), )); } - (Value::BigInt(a), Value::Int(_) | Value::Float(_)) => { + (JsValue::BigInt(a), JsValue::Int(_) | JsValue::Float(_)) => { + let a = bigint_payload(runtime, *a)?; return Ok(equal_result( kind, - compare_bigint_number(a, right.to_number()?) == Some(std::cmp::Ordering::Equal), + compare_bigint_number(&a, to_number_jsvalue(runtime, &right)?) + == Some(std::cmp::Ordering::Equal), )); } - (Value::Int(_) | Value::Float(_), Value::BigInt(b)) => { + (JsValue::Int(_) | JsValue::Float(_), JsValue::BigInt(b)) => { + let b = bigint_payload(runtime, *b)?; return Ok(equal_result( kind, - compare_bigint_number(b, left.to_number()?) == Some(std::cmp::Ordering::Equal), + compare_bigint_number(&b, to_number_jsvalue(runtime, &left)?) + == Some(std::cmp::Ordering::Equal), )); } - (Value::Bool(_), _) => left = Value::number(left.to_number()?), - (_, Value::Bool(_)) => right = Value::number(right.to_number()?), + (JsValue::Bool(_), _) => { + let number = to_number_jsvalue(runtime, &left)?; + left = jsvalue_number(number); + } + (_, JsValue::Bool(_)) => { + let number = to_number_jsvalue(runtime, &right)?; + right = jsvalue_number(number); + } ( - Value::Object(_), - Value::Int(_) - | Value::Float(_) - | Value::BigInt(_) - | Value::String(_) - | Value::Symbol(_), + JsValue::Object(_), + JsValue::Int(_) + | JsValue::Float(_) + | JsValue::BigInt(_) + | JsValue::String(_) + | JsValue::Symbol(_), ) => { return primitive( + runtime, left, ToPrimitiveHint::Default, NumericResume(Box::new(NumericResumeState { @@ -513,14 +629,15 @@ fn equality( ); } ( - Value::Int(_) - | Value::Float(_) - | Value::BigInt(_) - | Value::String(_) - | Value::Symbol(_), - Value::Object(_), + JsValue::Int(_) + | JsValue::Float(_) + | JsValue::BigInt(_) + | JsValue::String(_) + | JsValue::Symbol(_), + JsValue::Object(_), ) => { return primitive( + runtime, right, ToPrimitiveHint::Default, NumericResume(Box::new(NumericResumeState { @@ -573,6 +690,3 @@ mod tests { } } } - -// S11 all-domain protocol bound; inline completion stays allocation-free. -const _: () = assert!(std::mem::size_of::() <= 64); diff --git a/src/engine/vm/predicate_driver.rs b/src/engine/vm/predicate_driver.rs index 01b6edb7..d2caa4b5 100644 --- a/src/engine/vm/predicate_driver.rs +++ b/src/engine/vm/predicate_driver.rs @@ -6,7 +6,7 @@ use super::{ use crate::engine::{ api::{Error, ErrorKind, runtime::Runtime}, object::ProxyBooleanKind, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -34,24 +34,28 @@ pub(super) fn start( let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; for offset in 0..2 { - runtime - .validate_value_domain( - execution.slots.peek(&frame.window, offset)?, - "property predicate input", - ) - .map_err(runtime_error_to_vm_error)?; + execution.slots.peek(&frame.window, offset)?; } let depth = execution.slots.depth(&frame.window); let right = execution.slots.pop(&mut frame.window)?; let left = execution.slots.pop(&mut frame.window)?; if kind == Kind::Instance { - let Value::Object(target) = right else { - return super::property_driver::throw_error( - runtime, - realm, - Error::new(ErrorKind::Type, "invalid 'instanceof' right operand"), - ) - .map(Progress::Call); + let target = match runtime + .root_and_release_jsvalue(right) + .map_err(runtime_error_to_vm_error)? + { + Value::Object(target) => target, + _ => { + runtime + .release_jsvalue(left) + .map_err(runtime_error_to_vm_error)?; + return super::property_driver::throw_error( + runtime, + realm, + Error::new(ErrorKind::Type, "invalid 'instanceof' right operand"), + ) + .map(Progress::Call); + } }; return super::proxy_get_driver::start_instance( runtime, execution, id, left, target, depth, @@ -63,7 +67,13 @@ pub(super) fn start( } else { (left, right) }; - if kind == Kind::Has && !matches!(base, Value::Object(_)) { + if kind == Kind::Has && !matches!(base, JsValue::Object(_)) { + runtime + .release_jsvalue(key) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; return super::property_driver::throw_error( runtime, realm, @@ -71,6 +81,12 @@ pub(super) fn start( ) .map(Progress::Call); } + let base = runtime + .root_and_release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; + let key = runtime + .root_and_release_jsvalue(key) + .map_err(runtime_error_to_vm_error)?; let input = Box::new(Input { base, key, @@ -110,7 +126,13 @@ pub(super) fn converted( .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(key) => key, - NativeConversion::Throw(value) => return Ok(CallStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(CallStep::Complete(Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))); + } }; if let Value::Object(object) = base { let op = if kind == Kind::Has { diff --git a/src/engine/vm/private_access.rs b/src/engine/vm/private_access.rs index 9872d601..1b8de7fc 100644 --- a/src/engine/vm/private_access.rs +++ b/src/engine/vm/private_access.rs @@ -9,7 +9,7 @@ use crate::engine::api::{ }; use crate::engine::code::bytecode::PrivateNameSource; use crate::engine::code::function::metadata::ClosureVariableKind; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum Access { @@ -22,7 +22,7 @@ pub(super) enum Access { pub(super) enum Outcome { Entered, Done, - Throw(Value), + Throw(JsValue), } #[inline(never)] @@ -60,7 +60,7 @@ pub(super) fn step( }; Ok(Outcome::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, )) } @@ -122,7 +122,10 @@ pub(super) fn step( return Err(super::bindings::lexical_read_only_error(runtime, name)?); } if access == Access::In { - let Value::Object(receiver) = base else { + let Value::Object(receiver) = runtime + .root_and_release_jsvalue(base) + .map_err(runtime_error_to_vm_error)? + else { return Err(Error::new(ErrorKind::Type, "invalid 'in' operand")); }; let present = if let Some(method) = @@ -148,23 +151,32 @@ pub(super) fn step( }; execution .slots - .push(&mut frame.window, Value::Bool(present))?; + .push(&mut frame.window, JsValue::Bool(present))?; } else { let method = private_bindings::optional_callable(runtime, source, kind)? .ok_or_else(|| Error::new(ErrorKind::Type, "not an object"))?; let receiver = private_bindings::branded_receiver(runtime, &method, kind, base)?; if access == Access::GetKeep { - execution - .slots - .push(&mut frame.window, Value::Object(receiver))?; + execution.slots.push( + &mut frame.window, + runtime + .into_jsvalue(Value::Object(receiver)) + .map_err(runtime_error_to_vm_error)?, + )?; } - execution - .slots - .push(&mut frame.window, Value::Object(method.as_object().clone()))?; + execution.slots.push( + &mut frame.window, + runtime + .into_jsvalue(Value::Object(method.as_object().clone())) + .map_err(runtime_error_to_vm_error)?, + )?; } return Ok(()); } - let Value::Object(receiver) = base else { + let Value::Object(receiver) = runtime + .root_and_release_jsvalue(base) + .map_err(runtime_error_to_vm_error)? + else { return Err(Error::new( ErrorKind::Type, if access == Access::In { @@ -190,7 +202,7 @@ pub(super) fn step( }; execution .slots - .push(&mut frame.window, Value::Bool(present))?; + .push(&mut frame.window, JsValue::Bool(present))?; } else { let name = name.ok_or_else(|| Error::new(ErrorKind::Type, "not a symbol"))?; match access { @@ -199,22 +211,45 @@ pub(super) fn step( .get_private_field_own(&receiver, &name) .map_err(runtime_error_to_vm_error)?; if access == Access::GetKeep { - execution - .slots - .push(&mut frame.window, Value::Object(receiver))?; + execution.slots.push( + &mut frame.window, + runtime + .into_jsvalue(Value::Object(receiver)) + .map_err(runtime_error_to_vm_error)?, + )?; } - execution.slots.push(&mut frame.window, value)?; + execution.slots.push( + &mut frame.window, + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + )?; } Access::Put => runtime - .set_private_field_own(&receiver, &name, value.unwrap()) + .set_private_field_own( + &receiver, + &name, + runtime + .root_and_release_jsvalue(value.unwrap()) + .map_err(runtime_error_to_vm_error)?, + ) .map_err(runtime_error_to_vm_error)?, Access::Define => { runtime - .define_private_field_own(&receiver, &name, value.unwrap()) + .define_private_field_own( + &receiver, + &name, + runtime + .root_and_release_jsvalue(value.unwrap()) + .map_err(runtime_error_to_vm_error)?, + ) .map_err(runtime_error_to_vm_error)?; - execution - .slots - .push(&mut frame.window, Value::Object(receiver))?; + execution.slots.push( + &mut frame.window, + runtime + .into_jsvalue(Value::Object(receiver)) + .map_err(runtime_error_to_vm_error)?, + )?; } Access::In => unreachable!(), } @@ -237,7 +272,7 @@ pub(super) fn step( }; Ok(Outcome::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, )) } @@ -274,16 +309,10 @@ fn enter_accessor( let callable = private_bindings::optional_callable(runtime, binding, kind)? .ok_or_else(|| Error::new(ErrorKind::Type, "not an object"))?; let setter = access == Access::Put; - let base = execution - .slots - .peek(&frame.window, usize::from(setter))? - .clone(); + let base = runtime + .dup_jsvalue(execution.slots.peek(&frame.window, usize::from(setter))?) + .map_err(runtime_error_to_vm_error)?; let receiver = private_bindings::branded_receiver(runtime, &callable, kind, base)?; - if setter { - runtime - .validate_value_domain(execution.slots.peek(&frame.window, 0)?, "call argument") - .map_err(runtime_error_to_vm_error)?; - } #[cfg(feature = "profiling")] let depth = execution.slots.depth(&frame.window); let frame = execution.frames.current_mut(id)?; @@ -292,7 +321,11 @@ fn enter_accessor( arguments .try_reserve_exact(1) .map_err(|_| Error::internal("setter arguments allocation failed"))?; - arguments.push(execution.slots.pop(&mut frame.window)?); + arguments.push( + runtime + .root_and_release_jsvalue(execution.slots.pop(&mut frame.window)?) + .map_err(runtime_error_to_vm_error)?, + ); } let base = execution.slots.pop(&mut frame.window)?; if access == Access::GetKeep { diff --git a/src/engine/vm/private_bindings.rs b/src/engine/vm/private_bindings.rs index 782d169a..cde9a4b3 100644 --- a/src/engine/vm/private_bindings.rs +++ b/src/engine/vm/private_bindings.rs @@ -4,7 +4,7 @@ use super::exception::runtime_error_to_vm_error; use crate::engine::api::{error::Error, runtime::Runtime}; use crate::engine::atom::Atom; use crate::engine::code::function::metadata::{ClosureVariableKind, VariableDefinition}; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; pub(in crate::engine::vm) fn validate_definition( definition: VariableDefinition, @@ -75,8 +75,8 @@ pub(in crate::engine::vm) fn initialize_callable( runtime: &Runtime, definition: VariableDefinition, binding: &mut FrameBinding, - home_object: Value, - callable_value: Value, + home_object: JsValue, + callable_value: JsValue, infer_name: bool, accepts_kind: impl FnOnce(ClosureVariableKind) -> bool, ) -> Result<(), Error> { @@ -86,11 +86,15 @@ pub(in crate::engine::vm) fn initialize_callable( "private-callable initializer referenced an incompatible binding", )); } - let Value::Object(home_object) = home_object else { + let JsValue::Object(home_object) = home_object else { return Err(Error::internal( "private-callable initializer did not receive a HomeObject", )); }; + let home_object = ObjectRef::from_owned_handle(runtime.clone(), home_object); + let callable_value = runtime + .root_and_release_jsvalue(callable_value) + .map_err(runtime_error_to_vm_error)?; let callable = runtime .callable_from_value(callable_value) .map_err(|error| Error::internal(error.to_string()))?; @@ -163,7 +167,9 @@ pub(super) fn step( runtime, definition, execution.slots.local_mut(&frame.window, index)?, - home.clone(), + runtime + .dup_jsvalue(&home) + .map_err(runtime_error_to_vm_error)?, callable, kind == Initialization::Method, |binding_kind| match kind { @@ -201,7 +207,7 @@ pub(super) fn step( }; Ok(Some(super::Completion::Throw( runtime - .new_native_error_from_error(frame.executable.realm, kind, &error) + .new_native_error_from_error_jsvalue(frame.executable.realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -367,16 +373,17 @@ pub(in crate::engine::vm) fn branded_receiver( runtime: &Runtime, callable: &CallableRef, kind: ClosureVariableKind, - base: Value, + base: JsValue, ) -> Result { use crate::engine::api::error::ErrorKind; // Resolve HomeObject's brand before validating the receiver, as QuickJS does. runtime .require_private_method_brand(callable, kind) .map_err(runtime_error_to_vm_error)?; - let Value::Object(receiver) = base else { + let JsValue::Object(receiver) = base else { return Err(Error::new(ErrorKind::Type, "not an object")); }; + let receiver = ObjectRef::from_owned_handle(runtime.clone(), receiver); if !runtime .check_private_method_brand(callable, &receiver, kind) .map_err(runtime_error_to_vm_error)? diff --git a/src/engine/vm/property_driver.rs b/src/engine/vm/property_driver.rs index 281d6491..1cfab15c 100644 --- a/src/engine/vm/property_driver.rs +++ b/src/engine/vm/property_driver.rs @@ -13,7 +13,7 @@ use crate::engine::{ code::function::metadata::FunctionKind, heap::ContextId, object::{OrdinaryRead, PropertyKey}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, conversion::NativeConversion}, }; #[derive(Clone, Copy)] @@ -43,8 +43,8 @@ impl PropertyProgress { /// Converted inputs stay owned after ToPrimitive's reply, even if lookup next /// reaches a Proxy or a callable whose domain continuation is still pending. pub(super) struct ConvertedRead { - pub base: Value, - pub key: Value, + pub base: JsValue, + pub key: JsValue, pub keep_receiver: bool, pub keep_key: bool, } @@ -61,7 +61,7 @@ pub(super) fn throw_error( }; Ok(CallStep::Complete(Completion::Throw( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ))) } @@ -139,6 +139,7 @@ pub(super) fn read_progress_selected( keep_receiver, value, )?; + let _ = runtime; if let Some(count) = count { // GetField2 completed even if a later fallible argument // retain fails at its own canonical PC. @@ -151,6 +152,7 @@ pub(super) fn read_progress_selected( frame.fault_pc = start + offset + 1; frame.resume_pc = frame.fault_pc; let literal = super::method_arguments::argument( + runtime, slots, &executable.code[frame.fault_pc], )?; @@ -171,13 +173,10 @@ pub(super) fn read_progress_selected( if method_call.is_none() { record_read_completion(depth); } - #[cfg(feature = "profiling")] - if preserved_receiver.is_some() { - // One actual driver-scope owner drop, not a claim that - // this was the runtime's final root or that GC ran. - crate::engine::api::profiling::record_owned_execution_event( - "linked_read_base_owner_drop", - ); + if !keep_receiver && let Some(value) = preserved_receiver.take() { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; } return Ok(method_call .map(PropertyProgress::MethodCall) @@ -195,20 +194,30 @@ pub(super) fn read_progress_selected( super::BytecodePc::new(frame.fault_pc), ) .map_err(runtime_error_to_vm_error)?; - drop(retained_key); - drop(preserved_receiver); + if let Some(value) = retained_key.take() { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + } + if let Some(value) = preserved_receiver.take() { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + } return Err(error); } } } let base = execution.slots.peek(&frame.window, usize::from(computed))?; - if computed && matches!(base, Value::Null | Value::Undefined) { + if computed && matches!(base, JsValue::Null | JsValue::Undefined) { let key = execution.slots.peek(&frame.window, 0)?; let message = if matches!(key_kind, ReadKey::Computed { keep_key: true }) - && !matches!(key, Value::Int(_) | Value::String(_) | Value::Symbol(_)) - { + && !matches!( + key, + JsValue::Int(_) | JsValue::String(_) | JsValue::Symbol(_) + ) { "value has no property" - } else if matches!(base, Value::Null) { + } else if matches!(base, JsValue::Null) { "cannot read property of null" } else { "cannot read property of undefined" @@ -246,17 +255,23 @@ pub(super) fn read_progress_selected( } ReadKey::Computed { keep_key } => { let value = execution.slots.peek(&frame.window, 0)?; - if matches!(value, Value::Object(_)) { + if matches!(value, JsValue::Object(_)) { return Err(Error::internal( "object property key did not enter its conversion operation", )); } + let owned = runtime + .dup_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; let key = match runtime - .native_to_property_key(realm, value.clone()) + .native_to_property_key_jsvalue(realm, owned) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(key) => key, NativeConversion::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(PropertyProgress::Deferred(CallStep::Complete( Completion::Throw(value), ))); @@ -264,8 +279,13 @@ pub(super) fn read_progress_selected( }; let retained = keep_key .then(|| match value { - Value::Int(_) | Value::String(_) | Value::Symbol(_) => Ok(value.clone()), - value => value.to_js_string().map(Value::String), + JsValue::Int(_) | JsValue::String(_) | JsValue::Symbol(_) => runtime + .dup_jsvalue(value) + .map_err(runtime_error_to_vm_error), + value => Ok(super::numeric::allocate_string_jsvalue( + runtime, + super::numeric::to_js_string_jsvalue(runtime, value)?, + )?), }) .transpose()?; (Some(std::borrow::Cow::Owned(key)), retained) @@ -274,7 +294,7 @@ pub(super) fn read_progress_selected( // Lookup borrows the original rooted operand. Only a pending callback // needs a second receiver owner; completed reads move this slot directly. let read = match selected_read.map(Ok).unwrap_or_else(|| { - runtime.prepare_value_property_read_selected( + runtime.prepare_value_property_read_selected_jsvalue( realm, base, key.as_deref() @@ -297,18 +317,21 @@ pub(super) fn read_progress_selected( }; match read { OrdinaryRead::Complete(value) => complete_read( + runtime, execution, id, None, retained_key, keep_receiver, 1 + usize::from(computed), - value.unwrap_or(Value::Undefined), + value.unwrap_or(JsValue::Undefined), depth, ) .map(|()| PropertyProgress::Completed), read => { - let preserved_receiver = base.clone(); + let preserved_receiver = runtime + .dup_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; read_pending( runtime, execution, @@ -340,7 +363,7 @@ pub(super) fn read_converted( keep_receiver, keep_key, } = *input; - if matches!(key, Value::Object(_)) { + if matches!(key, JsValue::Object(_)) { return Err(Error::internal( "ToPrimitive returned an object property key", )); @@ -352,18 +375,28 @@ pub(super) fn read_converted( // if ToPrimitive returned an Int. Direct Int keys retain their original tag. let retained = if keep_key { Some(match &key { - Value::Symbol(_) | Value::String(_) => key.clone(), - value => Value::String(value.to_js_string()?), + JsValue::Symbol(_) | JsValue::String(_) => runtime + .dup_jsvalue(&key) + .map_err(runtime_error_to_vm_error)?, + value => super::numeric::allocate_string_jsvalue( + runtime, + super::numeric::to_js_string_jsvalue(runtime, value)?, + )?, }) } else { None }; let key = match runtime - .native_to_property_key(realm, key) + .native_to_property_key_jsvalue(realm, key) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(key) => key, - NativeConversion::Throw(value) => return Ok(CallStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + return Ok(CallStep::Complete(Completion::Throw(value))); + } }; finish_read( runtime, @@ -384,15 +417,15 @@ fn finish_read( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - base: Value, + base: JsValue, key: PropertyKey, - retained_key: Option, + retained_key: Option, keep_receiver: bool, consume: usize, depth: usize, ) -> Result { let realm = execution.frames.current_mut(id)?.executable.realm; - let read = match runtime.prepare_value_property_read_borrowed(realm, &base, &key) { + let read = match runtime.prepare_value_property_read_borrowed_jsvalue(realm, &base, &key) { Ok(read) => read, Err(error) => { return throw_error(runtime, realm, runtime_error_to_vm_error(error)) @@ -418,10 +451,10 @@ pub(super) fn read_prepared( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - preserved_receiver: Value, + preserved_receiver: JsValue, key: PropertyKey, read: OrdinaryRead, - retained_key: Option, + retained_key: Option, keep_receiver: bool, consume: usize, depth: usize, @@ -446,23 +479,24 @@ fn read_prepared_progress( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - preserved_receiver: Value, + preserved_receiver: JsValue, key: PropertyKey, read: OrdinaryRead, - retained_key: Option, + retained_key: Option, keep_receiver: bool, consume: usize, depth: usize, ) -> Result { match read { OrdinaryRead::Complete(value) => complete_read( + runtime, execution, id, Some(preserved_receiver), retained_key, keep_receiver, consume, - value.unwrap_or(Value::Undefined), + value.unwrap_or(JsValue::Undefined), depth, ) .map(|()| PropertyProgress::Completed), @@ -485,13 +519,14 @@ fn read_prepared_progress( // Keep the no-callback path out of the callback dispatcher's large native frame. #[allow(clippy::too_many_arguments)] fn complete_read( + runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - mut preserved_receiver: Option, - mut retained_key: Option, + mut preserved_receiver: Option, + mut retained_key: Option, keep_receiver: bool, consume: usize, - value: Value, + value: JsValue, depth: usize, ) -> Result<(), Error> { if consume > 2 || (preserved_receiver.is_none() && consume == 0) { @@ -503,17 +538,17 @@ fn complete_read( let discarded = { let mut slots = transaction.slots(); // Moving the base preserves its owner until after result publication. - // The remaining removed key may be released inside this window only - // when its tag proves that it cannot free storage or drain deferred GC. + // A scalar key's release cannot free arena storage or drain deferred + // GC; heap-backed keys take the outside-window release below. let immediate_key = preserved_receiver.is_none() && (consume == 1 || matches!( slots.peek(0)?, - Value::Undefined - | Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_) + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) )); let mut discarded = [None, None]; for destination in discarded.iter_mut().take(consume) { @@ -523,7 +558,13 @@ fn complete_read( preserved_receiver = discarded[consume - 1].take(); } if immediate_key { - drop(discarded); + for slot in discarded.iter_mut() { + if let Some(taken) = slot.take() { + runtime + .release_jsvalue(taken) + .map_err(runtime_error_to_vm_error)?; + } + } publish_read_result( &mut slots, &mut frame.resume_pc, @@ -534,13 +575,23 @@ fn complete_read( &mut value, )?; record_read_completion(depth); + let _ = slots; + if !keep_receiver && let Some(receiver) = preserved_receiver.take() { + runtime + .release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + } return Ok(()); } discarded }; // Preserve original pop/release order outside RunSlots for owning keys - // and externally prepared reads. The base and normalized key stay rooted. - drop(discarded); + // and externally prepared reads. The base and normalized key stay owned. + for slot in discarded.into_iter().flatten() { + runtime + .release_jsvalue(slot) + .map_err(runtime_error_to_vm_error)?; + } let mut slots = transaction.slots(); publish_read_result( &mut slots, @@ -552,6 +603,12 @@ fn complete_read( &mut value, )?; record_read_completion(depth); + let _ = slots; + if !keep_receiver && let Some(receiver) = preserved_receiver.take() { + runtime + .release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + } Ok(()) } @@ -561,10 +618,10 @@ fn publish_read_result( slots: &mut super::stack::RunSlots<'_>, resume_pc: &mut usize, fault_pc: usize, - preserved_receiver: &mut Option, - retained_key: &mut Option, + preserved_receiver: &mut Option, + retained_key: &mut Option, keep_receiver: bool, - value: &mut Option, + value: &mut Option, ) -> Result<(), Error> { if keep_receiver { slots.push_pending(preserved_receiver)?; @@ -595,10 +652,10 @@ fn read_pending( runtime: &Runtime, execution: &mut RunningExecution, id: FrameId, - preserved_receiver: Value, + preserved_receiver: JsValue, key: Option, read: OrdinaryRead, - retained_key: Option, + retained_key: Option, keep_receiver: bool, consume: usize, depth: usize, @@ -610,7 +667,7 @@ fn read_pending( let mut proxy_callback = None; let mut native_callback = None; let value = match read { - OrdinaryRead::Complete(value) => Some(value.unwrap_or(Value::Undefined)), + OrdinaryRead::Complete(value) => Some(value.unwrap_or(JsValue::Undefined)), OrdinaryRead::Call { getter, receiver } => { if let Some(call) = super::call::ordinary::OrdinaryCall::select_callback(runtime, getter.as_object()) @@ -644,7 +701,11 @@ fn read_pending( )? { NativeConversion::Value(call) => call, NativeConversion::Throw(value) => { - return Ok(CallStep::Complete(Completion::Throw(value))); + return Ok(CallStep::Complete(Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))); } }; let normal = match &classification { @@ -677,7 +738,7 @@ fn read_pending( callable, receiver, arguments, - new_target: Value::Undefined, + new_target: JsValue::Undefined, bytecode, closure_slots, caller_realm: realm, @@ -711,12 +772,19 @@ fn read_pending( }; let frame = execution.frames.current_mut(id)?; for _ in 0..consume { - execution.slots.pop(&mut frame.cold.window)?; + let operand = execution.slots.pop(&mut frame.cold.window)?; + runtime + .release_jsvalue(operand) + .map_err(runtime_error_to_vm_error)?; } if keep_receiver { execution .slots .push(&mut frame.cold.window, preserved_receiver)?; + } else { + runtime + .release_jsvalue(preserved_receiver) + .map_err(runtime_error_to_vm_error)?; } if let Some(key) = retained_key { execution.slots.push(&mut frame.cold.window, key)?; @@ -750,7 +818,9 @@ fn read_pending( if let Some((call, receiver)) = ordinary_callback { let entry = call.prepare_callback( &mut execution.call_storage, - receiver, + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, Vec::new(), realm, ReturnTarget { diff --git a/src/engine/vm/property_keys.rs b/src/engine/vm/property_keys.rs index 38d12a80..f55b3570 100644 --- a/src/engine/vm/property_keys.rs +++ b/src/engine/vm/property_keys.rs @@ -3,7 +3,48 @@ use crate::engine::api::{error::Error, runtime::Runtime}; use crate::engine::object::PropertyKey; use crate::engine::value::Value; -pub(super) fn canonical(runtime: &Runtime, value: &Value) -> Result { +pub(super) fn canonical( + runtime: &Runtime, + value: &crate::engine::value::JsValue, +) -> Result { + if let Some(key) = runtime.immediate_numeric_property_key_jsvalue(value) { + return Ok(key); + } + match value { + crate::engine::value::JsValue::Symbol(index) => { + let atom = runtime + .0 + .state + .borrow() + .atoms + .brand(*index) + .map_err(|error| Error::internal(error.to_string()))?; + PropertyKey::from_borrowed_atom(runtime.clone(), atom) + .map_err(|error| Error::internal(error.to_string())) + } + crate::engine::value::JsValue::String(id) => { + let string = runtime + .0 + .state + .borrow() + .heap + .string(*id) + .map_err(|error| Error::internal(error.to_string()))? + .clone(); + runtime + .intern_property_key_js_string(&string) + .map_err(|error| Error::internal(error.to_string())) + } + value => { + let rooted = runtime + .root_value(value) + .map_err(|error| Error::internal(error.to_string()))?; + canonical_rooted(runtime, &rooted) + } + } +} + +fn canonical_rooted(runtime: &Runtime, value: &Value) -> Result { if let Some(key) = runtime.immediate_numeric_property_key(value) { return Ok(key); } @@ -81,12 +122,22 @@ pub(super) fn set_name( "function-name opcode referenced a non-string constant", )); }; - name.clone() + // The published bytecode node owns the constant-pool edge, so + // the trusted read clones the payload Rc without retaining. + runtime.0.state.borrow().heap.string_fast(*name).clone() + } + None => { + let key = runtime + .root_value(execution.slots.peek(&frame.window, 1)?) + .map_err(runtime_error_to_vm_error)?; + computed_name(runtime, &key)? } - None => computed_name(runtime, execution.slots.peek(&frame.window, 1)?)?, }; + let target = runtime + .root_value(execution.slots.peek(&frame.window, 0)?) + .map_err(runtime_error_to_vm_error)?; runtime - .define_object_name(execution.slots.peek(&frame.window, 0)?, &name) + .define_object_name(&target, &name) .map_err(runtime_error_to_vm_error) })(); match result { diff --git a/src/engine/vm/property_write_driver.rs b/src/engine/vm/property_write_driver.rs index ec7a9e4f..58ec2bf8 100644 --- a/src/engine/vm/property_write_driver.rs +++ b/src/engine/vm/property_write_driver.rs @@ -6,7 +6,7 @@ use super::{ use crate::engine::{ api::{Error, ErrorKind, runtime::Runtime}, object::PropertyKey, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, }; pub(super) struct ConvertedWrite { @@ -45,16 +45,21 @@ pub(super) fn write_progress( PropertyKey::from_borrowed_atom(runtime.clone(), atom) .map_err(|error| Error::internal(error.to_string()))? } else { - let value = execution.slots.peek(&parent.window, 1)?.clone(); - if matches!(value, Value::Object(_)) { + let value = runtime + .dup_jsvalue(execution.slots.peek(&parent.window, 1)?) + .map_err(runtime_error_to_vm_error)?; + if matches!(value, JsValue::Object(_)) { return Err(Error::internal("object write key did not enter conversion")); } match runtime - .native_to_property_key(realm, value) + .native_to_property_key_jsvalue(realm, value) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(key) => key, NativeConversion::Throw(value) => { + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; return Ok(PropertyProgress::Deferred(CallStep::Complete( Completion::Throw(value), ))); @@ -73,7 +78,17 @@ pub(super) fn write_progress( }; (slots.pop()?, value, discarded_key) }; - drop(discarded_key); + if let Some(discarded_key) = discarded_key { + runtime + .release_jsvalue(discarded_key) + .map_err(runtime_error_to_vm_error)?; + } + let base = runtime + .root_and_release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; dispatch(runtime, execution, frame, base, key, value, depth) } @@ -97,7 +112,13 @@ pub(super) fn converted( .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(key) => key, - NativeConversion::Throw(value) => return Ok(CallStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(CallStep::Complete(Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))); + } }; dispatch(runtime, execution, frame, base, key, value, depth) .map(PropertyProgress::into_call_step) diff --git a/src/engine/vm/protocol.rs b/src/engine/vm/protocol.rs index 713361fb..2f4d89ab 100644 --- a/src/engine/vm/protocol.rs +++ b/src/engine/vm/protocol.rs @@ -1,4 +1,8 @@ -use crate::engine::{api::Error, object::ObjectRef, value::Value}; +use crate::engine::{ + api::Error, + object::ObjectRef, + value::{JsValue, Value}, +}; /// Caller state attached to one original direct-eval invocation. /// @@ -13,16 +17,44 @@ pub(crate) struct DirectEvalInvocation { pub input: Value, pub environment: u16, pub this_value: Value, - pub new_target: Value, pub caller_strict: bool, } pub(crate) struct CallInput { - pub this_value: Value, - pub new_target: Value, + runtime: crate::engine::api::runtime::Runtime, + pub this_value: JsValue, + pub new_target: JsValue, pub callee_global: Option, } +impl CallInput { + pub(in crate::engine::vm) fn new( + runtime: &crate::engine::api::runtime::Runtime, + this_value: JsValue, + new_target: JsValue, + callee_global: Option, + ) -> Self { + Self { + runtime: runtime.clone(), + this_value, + new_target, + callee_global, + } + } +} + +impl Drop for CallInput { + /// Release the two internal call edges the frame still owns when the cold + /// owner is recycled. Releases are defer-safe and never run JavaScript; + /// consumed slots have already been replaced with `Undefined`. + fn drop(&mut self) { + let this_value = std::mem::replace(&mut self.this_value, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(this_value); + let new_target = std::mem::replace(&mut self.new_target, JsValue::Undefined); + let _ = self.runtime.release_jsvalue(new_target); + } +} + impl CallInput { pub(in crate::engine::vm) fn callee_global( &mut self, diff --git a/src/engine/vm/proxy_get_driver.rs b/src/engine/vm/proxy_get_driver.rs index 61ae173a..376c1ad4 100644 --- a/src/engine/vm/proxy_get_driver.rs +++ b/src/engine/vm/proxy_get_driver.rs @@ -18,7 +18,7 @@ use crate::engine::object::{ OrdinaryPropertyDescriptor, PreparedHas, ProxyBooleanKind, ProxyBooleanResume, ProxyBooleanStep, }; use crate::engine::value::conversion::descriptor::{DescriptorResume, DescriptorStep}; -use crate::engine::value::{Value, conversion::NativeConversion}; +use crate::engine::value::{JsValue, Value, conversion::NativeConversion}; use crate::engine::object::{ProxyPrototypeKind, ProxyPrototypeStep}; @@ -214,12 +214,14 @@ enum Finish { } fn finish_numeric( + runtime: &Runtime, execution: &mut RunningExecution, frame: FrameId, - value: Value, - previous: Option, + value: JsValue, + previous: Option, _depth: usize, ) -> Result { + let _ = runtime; super::frame_operations::commit_numeric_output(execution, frame, value, previous, _depth)?; Ok(CallStep::Entered) } @@ -290,7 +292,13 @@ pub(super) fn start( object, key, receiver, - execution.slots.take_argument_buffer(3)?, + execution + .slots + .take_argument_buffer(3)? + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)?; advance( @@ -330,7 +338,11 @@ pub(super) fn start_owned_read( let step = Step::Read { object: Some(object.clone()), key: Some(key), - receiver: Some(receiver), + receiver: Some( + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + ), resume: Some(Resume::ReadOwner(object)), }; let result = advance( @@ -505,7 +517,13 @@ pub(super) fn start_conversion( object, key, receiver, - execution.slots.take_argument_buffer(3)?, + execution + .slots + .take_argument_buffer(3)? + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)?; advance( @@ -527,8 +545,8 @@ pub(super) fn start_call( execution: &mut RunningExecution, frame: FrameId, proxy: ObjectRef, - receiver: Value, - arguments: Vec, + receiver: crate::engine::value::JsValue, + arguments: Vec, tail: bool, depth: usize, ) -> Result { @@ -552,8 +570,8 @@ pub(super) fn start_callback_call( execution: &mut RunningExecution, frame: FrameId, callable: crate::engine::object::CallableRef, - receiver: Value, - arguments: Vec, + receiver: crate::engine::value::JsValue, + arguments: Vec, tail: bool, depth: usize, ) -> Result { @@ -775,8 +793,9 @@ pub(super) fn start_waitable_native_call( // Release the abandoned inner state while its outer // activation still owns the protocol call. The reply // resume is likewise consumed before the outer finish. - records[0].step = - Step::Complete(Some(Completion::Return(Value::Undefined))); + records[0].step = Step::Complete(Some(Completion::Return( + crate::engine::value::JsValue::Undefined, + ))); if let Some(mut parent) = parent.take() { let outer = parent.call.take().expect("outer replace activation"); drop(parent); @@ -804,7 +823,9 @@ pub(super) fn start_waitable_native_call( native::install_waiting(&mut query, call, resume); let step = std::mem::replace( &mut records[0].step, - Step::Complete(Some(Completion::Return(Value::Undefined))), + Step::Complete(Some(Completion::Return( + crate::engine::value::JsValue::Undefined, + ))), ); execution.query_storage.recycle_native_wait(records); #[cfg(feature = "profiling")] @@ -868,13 +889,21 @@ pub(super) fn start_apply( let realm = execution.frames.current_mut(frame)?.executable.realm; let result = (|| { let parent = execution.frames.current_mut(frame)?; + // The operands stay in their slots; the spread machine borrows rooted + // copies while `start_instruction` consumes the slot owners. let step = crate::engine::builtins::InvokeStep::start_spread( runtime, realm, kind, - execution.slots.peek(&parent.window, 2)?.clone(), - execution.slots.peek(&parent.window, 1)?.clone(), - execution.slots.peek(&parent.window, 0)?.clone(), + runtime + .root_value(execution.slots.peek(&parent.window, 2)?) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_value(execution.slots.peek(&parent.window, 1)?) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_value(execution.slots.peek(&parent.window, 0)?) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)?; start_instruction(runtime, execution, frame, step.into(), 3) @@ -891,8 +920,8 @@ pub(super) fn start_construct( execution: &mut RunningExecution, frame: FrameId, target: super::call::ConstructorRef, - new_target: Value, - arguments: Vec, + new_target: crate::engine::value::JsValue, + arguments: Vec, operand_count: usize, ) -> Result { let realm = execution.frames.current_mut(frame)?.executable.realm; @@ -928,9 +957,13 @@ fn start_instruction( .ok_or_else(|| Error::internal("instruction operation identity exhausted"))?; parent.property_generation = identity; let depth = execution.slots.depth(&parent.window); - // The request owns every source value before any window owner is released. + // The request owns rooted/duplicated source values before any window owner + // is released, so releasing the consumed slot owners cannot invalidate it. for _ in 0..operand_count { - execution.slots.pop(&mut parent.window)?; + let owner = execution.slots.pop(&mut parent.window)?; + runtime + .release_jsvalue(owner) + .map_err(runtime_error_to_vm_error)?; } advance( runtime, @@ -948,8 +981,8 @@ pub(super) fn start_native_conversion_call( execution: &mut RunningExecution, frame: FrameId, callable: crate::engine::object::CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, wait: super::conversion_driver::ConversionWait, ) -> Result { start_owned_callback( @@ -968,8 +1001,8 @@ fn start_owned_callback( execution: &mut RunningExecution, frame: FrameId, callable: crate::engine::object::CallableRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, finish: Finish, ) -> Result { let parent = execution.frames.current_mut(frame)?; @@ -1001,8 +1034,8 @@ pub(super) fn start_conversion_call( execution: &mut RunningExecution, frame: FrameId, proxy: ObjectRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, wait: super::conversion_driver::ConversionWait, ) -> Result { start_proxy_call( @@ -1021,8 +1054,8 @@ fn start_proxy_call( execution: &mut RunningExecution, frame: FrameId, proxy: ObjectRef, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, finish: Finish, ) -> Result { let parent = execution.frames.current_mut(frame)?; @@ -1033,6 +1066,14 @@ fn start_proxy_call( parent.property_generation = identity; let realm = parent.executable.realm; let result = (|| { + let receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + let arguments = arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; let step = crate::engine::object::ProxyCallStep::start(runtime, realm, proxy, receiver, arguments) .map_err(runtime_error_to_vm_error)?; @@ -1056,11 +1097,17 @@ pub(super) fn start_write( frame: FrameId, object: ObjectRef, key: PropertyKey, - value: Value, - receiver: Value, + value: JsValue, + receiver: JsValue, strict: bool, depth: usize, ) -> Result { + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + let receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; start_write_progress( runtime, execution, frame, object, key, value, receiver, strict, depth, ) @@ -1402,8 +1449,18 @@ pub(super) fn start_root( arguments, } => Step::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(receiver), - arguments: Some(arguments), + receiver: Some( + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + ), + arguments: Some( + arguments + .into_iter() + .map(|argument| runtime.into_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, + ), resume: Some(Resume::Identity), }, super::driver::RootOperation::Construct(normalized) => construct::prepared( @@ -1421,7 +1478,11 @@ pub(super) fn start_root( } => Step::Read { object: Some(object), key: Some(key), - receiver: Some(receiver), + receiver: Some( + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + ), resume: Some(Resume::Identity), }, super::driver::RootOperation::Own { object, key } => Step::Descriptor { @@ -1447,8 +1508,16 @@ pub(super) fn start_root( } => Step::Set { object: Some(object), key: Some(key), - value: Some(value), - receiver: Some(receiver), + value: Some( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + receiver: Some( + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + ), resume: Some(Resume::RootSet), }, @@ -1759,8 +1828,8 @@ fn invoke( identity: u64, query: &mut Query, target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: JsValue, + arguments: Vec, resume: Resume, next_step: &mut Step, ) -> Result { @@ -1785,7 +1854,17 @@ fn invoke( .map_err(|_| Error::internal("property continuation allocation failed"))?; query.parents.push(resume); step = crate::engine::object::ProxyCallStep::start( - runtime, realm, proxy, receiver, arguments, + runtime, + realm, + proxy, + runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); @@ -1834,14 +1913,33 @@ fn invoke( resume, }); } + let normalized_receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + let normalized_arguments = arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; let super::call::NormalizedCallback { callable, receiver, arguments, classification, - } = match super::call::normalize_callback(runtime, realm, callable, receiver, arguments)? { + } = match super::call::normalize_callback( + runtime, + realm, + callable, + normalized_receiver, + normalized_arguments, + )? { NativeConversion::Value(call) => call, NativeConversion::Throw(value) => { + // The normalization boundary threw a public root; transfer it into + // the internal completion without a retain/release pair. + let value = runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; step = resume .resume(runtime, Completion::Throw(value)) .map_err(runtime_error_to_vm_error)?; @@ -1865,6 +1963,16 @@ fn invoke( .try_reserve(1) .map_err(|_| Error::internal("property continuation allocation failed"))?; query.parents.push(resume); + // The proxy-call machine consumes public roots; the normalized + // internal owners are rooted at this sub-driver boundary. + let receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + let arguments = arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; step = crate::engine::object::ProxyCallStep::start( runtime, realm, @@ -1898,7 +2006,11 @@ fn invoke( super::call::NativeInvocation::Call { this_value: receiver, }, - arguments, + arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, resume, next_step, )?; @@ -1918,7 +2030,8 @@ fn invoke( .map_err(|error| Error::internal(error.to_string()))? .metadata; let kind = metadata.function_kind; - let module_link = metadata.is_module && receiver == Value::Bool(true); + let module_link = + metadata.is_module && matches!(receiver, crate::engine::value::JsValue::Bool(true)); { if !execution .frames @@ -1953,7 +2066,7 @@ fn invoke( arguments, bytecode, closure_slots, - new_target: Value::Undefined, + new_target: crate::engine::value::JsValue::Undefined, caller_realm: realm, return_to: ReturnTarget { owner, @@ -2005,14 +2118,8 @@ fn invoke( .heap .context(realm) .map_err(|error| Error::internal(error.to_string()))?; - runtime - .validate_value_domain(&receiver, "call this value") - .map_err(runtime_error_to_vm_error)?; - for argument in &arguments { - runtime - .validate_value_domain(argument, "call argument") - .map_err(runtime_error_to_vm_error)?; - } + // Internal values carry no runtime branding; the slot authentication + // above already proved every operand owner. let completion = if runtime.native_call_would_overflow(target) { overflow(runtime, realm)? } else { @@ -2021,16 +2128,35 @@ fn invoke( } else { defining_realm }; - runtime + // The native ABI consumes public roots; the normalized internal owners + // are rooted at this leaf boundary and released after the call. + let rooted_receiver = runtime + .root_value(&receiver) + .map_err(runtime_error_to_vm_error)?; + let rooted_arguments = arguments + .iter() + .map(|argument| runtime.root_value(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; + let completion = runtime .call_native_function( &callable, execution_realm, target, min_readable_args, - receiver, - &arguments, + rooted_receiver, + &rooted_arguments, ) - .map_err(runtime_error_to_vm_error)? + .map_err(runtime_error_to_vm_error); + runtime + .release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + for argument in arguments { + runtime + .release_jsvalue(argument) + .map_err(runtime_error_to_vm_error)?; + } + completion? }; step = resume .resume(runtime, completion) @@ -2042,7 +2168,7 @@ fn invoke( fn overflow(runtime: &Runtime, realm: crate::engine::heap::ContextId) -> Result { Ok(Completion::Throw( runtime - .new_native_error( + .new_native_error_jsvalue( realm, crate::engine::api::error::NativeErrorKind::Internal, "stack overflow", @@ -2166,8 +2292,8 @@ mod native_scope_tests { .unwrap(); let entry = BytecodeCallRequest { callable: parent_callable, - receiver: Value::Undefined, - new_target: Value::Undefined, + receiver: JsValue::Undefined, + new_target: JsValue::Undefined, arguments: Vec::new(), bytecode, closure_slots, @@ -2298,8 +2424,8 @@ mod native_scope_tests { .unwrap(); let entry = BytecodeCallRequest { callable: parent, - receiver: Value::Undefined, - new_target: Value::Undefined, + receiver: JsValue::Undefined, + new_target: JsValue::Undefined, arguments: Vec::new(), bytecode, closure_slots, @@ -2377,7 +2503,7 @@ mod native_scope_tests { target, min_readable_args, super::super::call::NativeInvocation::Call { - this_value: Value::Undefined, + this_value: JsValue::Undefined, }, &[], super::super::call::NativeInvokeMode::Ordinary, @@ -2429,9 +2555,12 @@ mod native_scope_tests { assert_eq!(query.realm, outer.realm); assert_eq!(query.continuation_depth(), 3); assert_eq!(runtime.0.state.borrow().active_frames.len(), 1); - let Step::Complete(Some(Completion::Throw(Value::Object(error)))) = step else { + let Step::Complete(Some(Completion::Throw(thrown))) = step else { panic!("expected captured error") }; + let Value::Object(error) = runtime.root_value(&thrown).unwrap() else { + panic!("expected error object") + }; assert_eq!( runtime.get_prototype_of(&error).unwrap().map(Value::Object), Some(prototype) @@ -2449,12 +2578,15 @@ mod native_scope_tests { .finish_native( &runtime, &mut slots, - Ok(Completion::Throw(Value::Object(error.clone()))), + Ok(Completion::Throw(runtime.dup_jsvalue(&thrown).unwrap())), ) .unwrap(); - assert!( - matches!(step, Step::Complete(Some(Completion::Throw(Value::Object(value)))) if value == error) - ); + let Step::Complete(Some(Completion::Throw(value))) = step else { + panic!("expected returned captured error") + }; + assert_eq!(value, thrown); + runtime.release_jsvalue(value).unwrap(); + runtime.release_jsvalue(thrown).unwrap(); assert_eq!(query.realm, caller.realm); assert_eq!(query.continuation_depth(), 1); assert!(runtime.0.state.borrow().active_frames.is_empty()); @@ -2465,7 +2597,7 @@ pub(super) fn start_iterator_read( runtime: &Runtime, execution: &mut RunningExecution, pending: super::iterator_driver::PendingIterator, - receiver: Value, + receiver: crate::engine::value::JsValue, key: PropertyKey, ) -> Result { start_iterator_query( @@ -2485,7 +2617,7 @@ pub(super) fn start_iterator_call( execution: &mut RunningExecution, pending: super::iterator_driver::PendingIterator, callable: crate::engine::object::CallableRef, - receiver: Value, + receiver: crate::engine::value::JsValue, ) -> Result { start_iterator_query( runtime, @@ -2505,8 +2637,8 @@ pub(super) fn start_iterator_invoke( execution: &mut RunningExecution, pending: super::iterator_driver::PendingIterator, target: DirectCallTarget, - receiver: Value, - arguments: Vec, + receiver: crate::engine::value::JsValue, + arguments: Vec, ) -> Result { start_iterator_query( runtime, @@ -2525,16 +2657,17 @@ pub(super) fn start_iterator_next( runtime: &Runtime, execution: &mut RunningExecution, pending: super::iterator_driver::PendingIterator, - iterator: Value, + iterator: crate::engine::value::JsValue, method: crate::engine::object::CallableRef, ) -> Result { - let Value::Object(iterator) = iterator else { + let crate::engine::value::JsValue::Object(iterator) = iterator else { return Err(Error::internal("iterator record lost object receiver")); }; let step = crate::engine::builtins::IteratorNextStep::start_callable( runtime, pending.realm(), - iterator, + crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), iterator) + .map_err(super::exception::heap_error_to_vm_error)?, method, ) .map_err(runtime_error_to_vm_error)?; @@ -2577,7 +2710,7 @@ pub(super) fn start_array_next_without_pending( callable: crate::engine::object::CallableRef, defining_realm: crate::engine::heap::ContextId, min_readable_args: u8, - iterator: Value, + iterator: crate::engine::value::JsValue, ) -> Result { use crate::engine::builtins::{IteratorNextResume, ObjectIteratorStep}; let realm = execution.frames.current_mut(frame)?.executable.realm; @@ -2590,7 +2723,9 @@ pub(super) fn start_array_next_without_pending( _ => return Err(Error::internal("iterator overflow did not throw")), }) } else { - let mut waiting = Step::Complete(Some(Completion::Return(Value::Undefined))); + let mut waiting = Step::Complete(Some(Completion::Return( + crate::engine::value::JsValue::Undefined, + ))); let mut waiting_call = None; let result = native::compact_array_next_into( runtime, @@ -2599,7 +2734,9 @@ pub(super) fn start_array_next_without_pending( callable, defining_realm, min_readable_args, - iterator, + runtime + .root_and_release_jsvalue(iterator) + .map_err(runtime_error_to_vm_error)?, &mut waiting, &mut waiting_call, )?; @@ -2632,7 +2769,7 @@ pub(super) fn start_array_next_without_pending( ); }; resume - .raw_completion(result) + .raw_completion(runtime, result) .map_err(runtime_error_to_vm_error)? .map_err(|_| Error::internal("Array-next returned an ordinary result object"))? }; @@ -2642,11 +2779,21 @@ pub(super) fn start_array_next_without_pending( ); let (value, done, abrupt) = match result { ObjectIteratorStep::Yield(value) => (value, false, None), - ObjectIteratorStep::Done => (Value::Undefined, true, None), - ObjectIteratorStep::Throw(value) => (Value::Undefined, false, Some(value)), + ObjectIteratorStep::Done => (crate::engine::value::JsValue::Undefined, true, None), + ObjectIteratorStep::Throw(value) => { + (crate::engine::value::JsValue::Undefined, false, Some(value)) + } }; - super::iterator_driver::finish_next(execution, frame, record_base, value, done, abrupt) - .map(Progress::Call) + super::iterator_driver::finish_next( + runtime, + execution, + frame, + record_base, + value, + done, + abrupt, + ) + .map(Progress::Call) })(); match finish_error(runtime, realm, result)? { Progress::Call(step) => Ok(step), @@ -2692,7 +2839,9 @@ fn start_array_next_direct( if !execution.query_storage.reserve_cached_native_entry()? { return Err(Error::internal("direct native entry lost reserved storage")); } - let mut waiting = Step::Complete(Some(Completion::Return(Value::Undefined))); + let mut waiting = Step::Complete(Some(Completion::Return( + crate::engine::value::JsValue::Undefined, + ))); let mut waiting_call = None; let result = native::begin_into( runtime, @@ -2704,7 +2853,7 @@ fn start_array_next_direct( min_readable_args, super::call::NativeInvokeMode::IteratorNextRaw, super::call::NativeInvocation::Call { - this_value: Value::Object(iterator), + this_value: JsValue::Object(iterator.into_handle()), }, Vec::new(), crate::engine::builtins::continuation::NativeOperation::ArrayNext, @@ -2735,7 +2884,7 @@ fn start_array_next_direct( ); }; match resume - .raw_completion(result) + .raw_completion(runtime, result) .map_err(runtime_error_to_vm_error)? { Ok(result) => result, @@ -2756,7 +2905,7 @@ fn start_array_next_direct( crate::engine::api::profiling::record_owned_execution_event( "iterator_native_completed_without_query", ); - super::iterator_driver::finish(execution, pending).map(Progress::Call) + super::iterator_driver::finish(runtime, execution, pending).map(Progress::Call) } fn iterator_query_identity(execution: &mut RunningExecution, frame: FrameId) -> Result { @@ -2833,7 +2982,7 @@ pub(super) fn start_instance( runtime: &Runtime, execution: &mut RunningExecution, frame: FrameId, - candidate: Value, + candidate: JsValue, target: ObjectRef, depth: usize, ) -> Result { @@ -2845,6 +2994,9 @@ pub(super) fn start_instance( .ok_or_else(|| Error::internal("instance query identity exhausted"))?; parent.property_generation = identity; let result = (|| { + let candidate = runtime + .root_and_release_jsvalue(candidate) + .map_err(runtime_error_to_vm_error)?; let step = crate::engine::builtins::InstanceStep::start(runtime, realm, candidate, target) .map_err(runtime_error_to_vm_error)?; advance( @@ -2873,15 +3025,24 @@ pub(super) fn start_object_copy( ) -> Result { let parent = execution.frames.current_mut(frame)?; let realm = parent.executable.realm; - let target = execution.slots.peek(&parent.window, target_depth)?.clone(); + // The copy machine borrows rooted copies; the slot owners stay live until + // the pops below consume them. + let target = runtime + .root_value(execution.slots.peek(&parent.window, target_depth)?) + .map_err(runtime_error_to_vm_error)?; let Value::Object(target) = target else { return Err(Error::internal( "CopyDataProperties target is not an object", )); }; - let source = execution.slots.peek(&parent.window, source_depth)?.clone(); + let source = runtime + .root_value(execution.slots.peek(&parent.window, source_depth)?) + .map_err(runtime_error_to_vm_error)?; let excluded = if let Some(depth) = excluded_depth { - let Value::Object(object) = execution.slots.peek(&parent.window, depth)?.clone() else { + let excluded = runtime + .root_value(execution.slots.peek(&parent.window, depth)?) + .map_err(runtime_error_to_vm_error)?; + let Value::Object(object) = excluded else { return Err(Error::internal( "CopyDataProperties exclusion is not an object", )); @@ -2899,18 +3060,26 @@ pub(super) fn start_object_copy( if excluded_depth.is_none() { let source = execution.slots.pop(&mut parent.window)?; if identity.is_none() { - // At exhaustion retain this already-rooted owner only long enough - // to restore the old failure input if a real wait is selected. + // At exhaustion retain this owner only long enough to restore the + // old failure input if a real wait is selected. rejected_source = Some(source); + } else { + // The normal source owner releases before any copy effects, as before. + runtime + .release_jsvalue(source) + .map_err(runtime_error_to_vm_error)?; } - // The normal source owner drops before any copy effects, as before. } let result = (|| { let step = step .advance_without_callback(runtime) .map_err(runtime_error_to_vm_error)?; if let crate::engine::builtins::ObjectCopyStep::Complete(completion) = step { - drop(rejected_source.take()); + if let Some(source) = rejected_source.take() { + runtime + .release_jsvalue(source) + .map_err(runtime_error_to_vm_error)?; + } return finish_instruction_call( execution, ReturnOwner::Frame(frame), @@ -2943,7 +3112,11 @@ pub(super) fn start_object_copy( Finish::Discard(depth), ) })(); - drop(rejected_source); + if let Some(source) = rejected_source.take() { + runtime + .release_jsvalue(source) + .map_err(runtime_error_to_vm_error)?; + } match finish_error(runtime, realm, result)? { Progress::Call(step) => Ok(step), Progress::Conversion(_) => Err(Error::internal("object copy returned conversion")), @@ -2960,6 +3133,14 @@ pub(super) fn start_vm_call( arguments: Vec, value_use: ReturnValue, ) -> Result { + let receiver = runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + let arguments = arguments + .into_iter() + .map(|argument| runtime.into_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; match start_owned_callback( runtime, execution, @@ -3022,7 +3203,8 @@ fn continue_iterator( use super::iterator_driver::IteratorAction; let (step, next) = match action { IteratorAction::Finish => { - return super::iterator_driver::finish(execution, pending).map(IteratorProgress::Done); + return super::iterator_driver::finish(runtime, execution, pending) + .map(IteratorProgress::Done); } IteratorAction::Read(receiver, key) => ( Step::ReadValue { @@ -3051,14 +3233,15 @@ fn continue_iterator( false, ), IteratorAction::Next(callable, receiver) => { - let Value::Object(iterator) = receiver else { + let JsValue::Object(iterator) = receiver else { return Err(Error::internal("iterator record lost object receiver")); }; ( crate::engine::builtins::IteratorNextStep::start_callable( runtime, pending.realm(), - iterator, + ObjectRef::from_borrowed_handle(runtime.clone(), iterator) + .map_err(super::exception::heap_error_to_vm_error)?, callable, ) .map_err(runtime_error_to_vm_error)? @@ -3091,7 +3274,7 @@ pub(super) fn start_numeric( crate::engine::api::profiling::record_owned_execution_event( "numeric_completed_without_query", ); - return match finish_numeric(execution, frame, value, previous, depth)? { + return match finish_numeric(runtime, execution, frame, value, previous, depth)? { CallStep::Entered => Ok(NumericProgress::Completed), _ => Err(Error::internal( "immediate numeric completion changed its frame protocol", @@ -3136,7 +3319,7 @@ pub(super) fn start_class_parent( frame: FrameId, ) -> Result { let step = Step::ReadValue { - receiver: Some(Value::Object(parent)), + receiver: Some(JsValue::Object(parent.into_handle())), key: Some( runtime .intern_property_key("prototype") @@ -3159,14 +3342,18 @@ pub(super) fn start_public_field( frame: FrameId, object: ObjectRef, key: PropertyKey, - value: Value, + value: crate::engine::value::JsValue, depth: usize, ) -> Result { let realm = execution.frames.current_mut(frame)?.executable.realm; let step = Step::Define { object: Some(object), key: Some(key), - descriptor: Some(Runtime::public_class_field_descriptor(value)), + descriptor: Some(Runtime::public_class_field_descriptor( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + )), resume: Some(Resume::PublicField), }; start_instruction_query( @@ -3229,6 +3416,7 @@ pub(super) fn start_for_in_query( crate::engine::api::profiling::record_owned_execution_event( "for_in_completed_without_query", ); + // The for-in machine now produces internal values directly. finish_for_in(execution, frame, value, done, depth) } ForInStep::Throw(value) => Ok(CallStep::Complete(Completion::Throw(value))), @@ -3275,16 +3463,17 @@ fn start_for_in_pending( fn finish_for_in( execution: &mut RunningExecution, frame: FrameId, - value: Value, + value: crate::engine::value::JsValue, done: Option, _depth: usize, ) -> Result { let parent = execution.frames.current_mut(frame)?; execution.slots.push(&mut parent.window, value)?; if let Some(done) = done { - execution - .slots - .push(&mut parent.window, Value::Bool(done))?; + execution.slots.push( + &mut parent.window, + crate::engine::value::JsValue::Bool(done), + )?; } parent.resume_pc = parent .fault_pc @@ -3324,8 +3513,14 @@ pub(super) fn start_import( .executable .ensure_root(runtime) .map_err(runtime_error_to_vm_error)?; - let options = execution.slots.peek(&parent.window, 0)?.clone(); - let specifier = execution.slots.peek(&parent.window, 1)?.clone(); + // The import machine borrows rooted copies; `start_instruction` consumes + // the two slot owners. + let options = runtime + .root_value(execution.slots.peek(&parent.window, 0)?) + .map_err(runtime_error_to_vm_error)?; + let specifier = runtime + .root_value(execution.slots.peek(&parent.window, 1)?) + .map_err(runtime_error_to_vm_error)?; let result = crate::engine::modules::import::ImportStep::start( runtime, realm, diff --git a/src/engine/vm/proxy_get_driver/construct.rs b/src/engine/vm/proxy_get_driver/construct.rs index 9e60bfec..c42cd3ba 100644 --- a/src/engine/vm/proxy_get_driver/construct.rs +++ b/src/engine/vm/proxy_get_driver/construct.rs @@ -2,8 +2,9 @@ use super::{ BytecodeCallRequest, CallableExecution, Completion, Error, NativeConversion, Next, OperationTarget, Query, Resume, ReturnOwner, ReturnTarget, ReturnValue, RunningExecution, - Runtime, Step, Value, overflow, runtime_error_to_vm_error, + Runtime, Step, overflow, runtime_error_to_vm_error, }; +use crate::engine::value::JsValue; use crate::engine::{ code::function::metadata::ConstructorKind, vm::call::{ConstructNewTarget, ConstructorRef, ConstructorTarget, NormalizedConstructor}, @@ -18,9 +19,14 @@ pub(super) fn start( realm: crate::engine::heap::ContextId, constructor: ConstructorRef, new_target: ConstructNewTarget, - arguments: Vec, + arguments: Vec, resume: Resume, ) -> Result { + #[cfg(debug_assertions)] + eprintln!( + "[ctor] start roots={:?}", + runtime.0.state.borrow().heap.debug_external_roots() + ); let normalized = match runtime .normalize_constructor(realm, constructor, new_target, arguments) .map_err(runtime_error_to_vm_error)? @@ -28,7 +34,14 @@ pub(super) fn start( NativeConversion::Value(result) => result, NativeConversion::Throw(value) => { return resume - .resume(runtime, Completion::Throw(value)) + .resume( + runtime, + Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error); } }; @@ -43,6 +56,11 @@ pub(super) fn prepared( normalized: NormalizedConstructor, resume: Resume, ) -> Result { + #[cfg(debug_assertions)] + eprintln!( + "[ctor] prepared-entry roots={:?}", + runtime.0.state.borrow().heap.debug_external_roots() + ); let NormalizedConstructor { target, new_target, @@ -74,7 +92,7 @@ pub(super) fn prepared( defining_realm: Some(defining_realm), min_readable_args: Some(min_readable_args), invocation: Some(crate::engine::vm::call::NativeInvocation::Construct { - new_target: new_target.value(), + new_target: new_target.into_value(), }), arguments: Some(arguments), resume: Some(resume), @@ -94,8 +112,8 @@ pub(super) fn prepared( .constructor_kind; let request = Box::new(BytecodeCallRequest { callable, - receiver: Value::Undefined, - new_target: new_target.value(), + receiver: JsValue::Undefined, + new_target: new_target.into_value(), arguments, bytecode, closure_slots, @@ -113,20 +131,24 @@ pub(super) fn prepared( )), ConstructorKind::Derived => Ok(Step::ConstructorReady { request: Some(request), - receiver: Some(Completion::Return(Value::Undefined)), + receiver: Some(Completion::Return(JsValue::Undefined)), derived: Some(true), resume: Some(resume), }), - ConstructorKind::Base if matches!(request.new_target, Value::Undefined) => { + ConstructorKind::Base if matches!(request.new_target, JsValue::Undefined) => { prototype( runtime, request, - Completion::Return(Value::Undefined), + Completion::Return(JsValue::Undefined), resume, ) } ConstructorKind::Base => Ok(Step::ReadValue { - receiver: Some(request.new_target.clone()), + receiver: Some( + runtime + .dup_jsvalue(&request.new_target) + .map_err(runtime_error_to_vm_error)?, + ), key: Some( runtime .pinned_property_key(crate::engine::atom::pinned::PinnedAtom::Prototype) @@ -148,12 +170,11 @@ pub(super) fn prototype( completion: Completion, resume: Resume, ) -> Result { + let new_target = runtime + .root_value(&request.new_target) + .map_err(runtime_error_to_vm_error)?; let receiver = runtime - .create_from_constructor_prototype_reply( - request.caller_realm, - &request.new_target, - completion, - ) + .create_from_constructor_prototype_reply(request.caller_realm, &new_target, completion) .map_err(runtime_error_to_vm_error)?; Ok(Step::ConstructorReady { request: Some(request), @@ -162,6 +183,9 @@ pub(super) fn prototype( resume: Some(resume), }) } +// The selected Step already owns this boxed request; consume it in place instead of +// moving its payload through the driver stack. +#[allow(clippy::boxed_local)] pub(super) fn ready( runtime: &Runtime, execution: &mut RunningExecution, @@ -188,12 +212,20 @@ pub(super) fn ready( .resume(runtime, overflow(runtime, request.caller_realm)?) .map_err(runtime_error_to_vm_error)?)); } - request.receiver = receiver.clone(); + // The child frame request owns one edge; the cold constructor-return + // record owns a duplicate. + request.receiver = runtime + .dup_jsvalue(&receiver) + .map_err(runtime_error_to_vm_error)?; let mut entry = request.prepare(runtime, &mut execution.call_storage)?; entry.cold.constructor_return = Some(if derived { crate::engine::vm::frame::ConstructorReturn::Derived } else { - crate::engine::vm::frame::ConstructorReturn::Base(receiver) + crate::engine::vm::frame::ConstructorReturn::Base( + runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + ) }); Ok(Ok(Next::Call { entry: Box::new(entry), diff --git a/src/engine/vm/proxy_get_driver/dispatch_conversion.rs b/src/engine/vm/proxy_get_driver/dispatch_conversion.rs index 29db6c4e..b3362830 100644 --- a/src/engine/vm/proxy_get_driver/dispatch_conversion.rs +++ b/src/engine/vm/proxy_get_driver/dispatch_conversion.rs @@ -1,6 +1,6 @@ //! Bounded native-stack dispatch for conversion requests. use super::{ - Error, Next, Query, Resume, ReturnOwner, RunningExecution, Runtime, Step, Value, + Error, Next, Query, Resume, ReturnOwner, RunningExecution, Runtime, Step, runtime_error_to_vm_error, }; @@ -57,9 +57,15 @@ pub(super) fn primitive( .try_reserve(1) .map_err(|_| Error::internal("argument continuation allocation failed"))?; query.parents.push(resume); - *step = crate::engine::builtins::ArgumentsStep::start(runtime, realm, value) - .map_err(runtime_error_to_vm_error)? - .into(); + *step = crate::engine::builtins::ArgumentsStep::start( + runtime, + realm, + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ) + .map_err(runtime_error_to_vm_error)? + .into(); continue; } Step::ArgumentsComplete(result) => { @@ -108,6 +114,9 @@ pub(super) fn primitive( // Only a request which can suspend needs a parent owner. Complete // results (including JS throws) use the same typed reply consumer. + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; let next = crate::engine::value::conversion::number::NumberStep::start( runtime, realm, value, ) @@ -245,7 +254,11 @@ pub(super) fn constructor( })?; query.parents.push(resume); *step = super::super::call::prototype::ProtoSourceStep::start( - runtime, realm, new_target, + runtime, + realm, + runtime + .root_and_release_jsvalue(new_target) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); @@ -297,10 +310,15 @@ pub(super) fn constructor( Error::internal("typed iterator method continuation allocation failed") })?; query.parents.push(resume); - *step = - crate::engine::builtins::TypedIteratorMethodStep::start(runtime, realm, source) - .map_err(runtime_error_to_vm_error)? - .into(); + *step = crate::engine::builtins::TypedIteratorMethodStep::start( + runtime, + realm, + runtime + .root_and_release_jsvalue(source) + .map_err(runtime_error_to_vm_error)?, + ) + .map_err(runtime_error_to_vm_error)? + .into(); continue; } Step::TypedIteratorMethodComplete(result) => { @@ -331,8 +349,15 @@ pub(super) fn constructor( })?; query.parents.push(resume); *step = crate::engine::builtins::TypedCollectStep::start( - realm, source, method, element, + runtime, + realm, + runtime + .root_and_release_jsvalue(source) + .map_err(runtime_error_to_vm_error)?, + method, + element, ) + .map_err(runtime_error_to_vm_error)? .into(); continue; } @@ -364,8 +389,13 @@ pub(super) fn constructor( *step = crate::engine::builtins::TypedSpeciesStep::create( runtime, realm, - constructor, - vec![Value::number(length as f64)], + runtime + .root_and_release_jsvalue(constructor) + .map_err(runtime_error_to_vm_error)?, + vec![ + crate::engine::value::number::operations::Number::compact(length as f64) + .into(), + ], Some(length), ) .map_err(runtime_error_to_vm_error)? diff --git a/src/engine/vm/proxy_get_driver/dispatch_execution.rs b/src/engine/vm/proxy_get_driver/dispatch_execution.rs index 3299fee8..3e7ac843 100644 --- a/src/engine/vm/proxy_get_driver/dispatch_execution.rs +++ b/src/engine/vm/proxy_get_driver/dispatch_execution.rs @@ -1,8 +1,9 @@ //! Bounded native-stack dispatch for execution requests. +use super::JsValue; use super::{ CallStep, Completion, DirectCallTarget, Error, Finish, IteratorProgress, Next, OperationTarget, Progress, Query, ReturnOwner, ReturnTarget, ReturnValue, RunningExecution, Runtime, Step, - Value, construct, continue_iterator, native_scope, overflow, runtime_error_to_vm_error, + construct, continue_iterator, native_scope, overflow, runtime_error_to_vm_error, }; #[inline(never)] @@ -139,9 +140,16 @@ pub(super) fn finish( let Some(Finish::Numeric(_depth)) = query.finish.take() else { return Err(Error::internal("numeric result lost its instruction")); }; - return super::finish_numeric(execution, owner.frame()?, value, previous, _depth) - .map(Progress::Call) - .map(Next::Done); + return super::finish_numeric( + runtime, + execution, + owner.frame()?, + value, + previous, + _depth, + ) + .map(Progress::Call) + .map(Next::Done); } Step::NativeRawComplete(result) => { let result = result.take().expect("selected Step field"); @@ -250,7 +258,7 @@ pub(super) fn activation( let arguments = arguments.take().expect("selected Step field"); let resume = resume.take().expect("selected Step field"); - *step = Step::Complete(Some(Completion::Return(Value::Undefined))); + *step = Step::Complete(Some(Completion::Return(JsValue::Undefined))); native_scope( runtime, execution, @@ -261,7 +269,11 @@ pub(super) fn activation( min_readable_args, mode, invocation, - arguments, + arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, resume, step, )?; @@ -363,8 +375,8 @@ pub(super) fn prepare( } let entry = super::BytecodeCallRequest { callable, - receiver: Value::Bool(true), - new_target: Value::Undefined, + receiver: JsValue::Bool(true), + new_target: JsValue::Undefined, arguments: Vec::new(), bytecode, closure_slots, @@ -413,7 +425,12 @@ pub(super) fn prepare( })?; query.parents.push(resume); *step = runtime - .prepare_intrinsic_promise_resolve(resolve_realm, value) + .prepare_intrinsic_promise_resolve( + resolve_realm, + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ) .map_err(runtime_error_to_vm_error)? .into(); continue; @@ -460,7 +477,15 @@ pub(super) fn prepare( .map_err(|_| Error::internal("constructor continuation allocation failed"))?; query.parents.push(resume); *step = crate::engine::object::ProxyConstructStep::start( - runtime, realm, target, new_target, arguments, + runtime, + realm, + target, + new_target, + arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); @@ -482,7 +507,11 @@ pub(super) fn prepare( this_value, } => Step::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(this_value), + receiver: Some( + runtime + .into_jsvalue(this_value) + .map_err(runtime_error_to_vm_error)?, + ), arguments: Some(Vec::new()), resume: Some(resume), }, @@ -493,8 +522,12 @@ pub(super) fn prepare( let value = value.take().expect("selected Step field"); let resume = resume.take().expect("selected Step field"); + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; *step = resume .html_dda( + runtime, runtime .value_is_html_dda(&value) .map_err(runtime_error_to_vm_error)?, diff --git a/src/engine/vm/proxy_get_driver/dispatch_iteration.rs b/src/engine/vm/proxy_get_driver/dispatch_iteration.rs index f4bf377f..2c612c68 100644 --- a/src/engine/vm/proxy_get_driver/dispatch_iteration.rs +++ b/src/engine/vm/proxy_get_driver/dispatch_iteration.rs @@ -1,4 +1,5 @@ //! Bounded native-stack dispatch for iteration requests. +use super::JsValue; use super::{ DirectCallTarget, Error, Finish, IteratorProgress, Next, Progress, Query, Resume, ReturnOwner, RunningExecution, Runtime, Step, Value, continue_iterator, runtime_error_to_vm_error, @@ -66,9 +67,15 @@ pub(super) fn advance( Error::internal("AggregateError continuation allocation failed") })?; query.parents.push(resume); - *step = crate::engine::builtins::AggregateStep::start(runtime, realm, iterable) - .map_err(runtime_error_to_vm_error)? - .into(); + *step = crate::engine::builtins::AggregateStep::start( + runtime, + realm, + runtime + .root_and_release_jsvalue(iterable) + .map_err(runtime_error_to_vm_error)?, + ) + .map_err(runtime_error_to_vm_error)? + .into(); continue; } Step::ArraySpecies { @@ -113,7 +120,11 @@ pub(super) fn advance( crate::engine::builtins::native::ArrayPushKind::Push, ), Value::Object(object), - vec![value], + vec![ + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ], ) .map_err(runtime_error_to_vm_error)? .into(); @@ -134,7 +145,12 @@ pub(super) fn advance( .map_err(|_| Error::internal("iterator continuation allocation failed"))?; query.parents.push(resume); *step = crate::engine::builtins::IteratorNextStep::start( - runtime, realm, iterator, method, + runtime, + realm, + iterator, + runtime + .root_and_release_jsvalue(method) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); @@ -168,7 +184,7 @@ pub(super) fn advance( && target.descriptor().cproto == crate::engine::builtins::native::NativeCProto::IteratorNext { - *step = Step::Complete(Some(super::Completion::Return(Value::Undefined))); + *step = Step::Complete(Some(super::Completion::Return(JsValue::Undefined))); super::native_scope( runtime, execution, @@ -179,7 +195,7 @@ pub(super) fn advance( min_readable_args, super::super::call::NativeInvokeMode::IteratorNextRaw, super::super::call::NativeInvocation::Call { - this_value: Value::Object(iterator), + this_value: JsValue::Object(iterator.into_handle()), }, Vec::new(), Resume::IteratorNext(resume), @@ -188,7 +204,7 @@ pub(super) fn advance( } else { *step = Step::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Object(iterator)), + receiver: Some(JsValue::Object(iterator.into_handle())), arguments: Some(Vec::new()), resume: Some(Resume::IteratorNext(resume)), }; @@ -240,7 +256,14 @@ pub(super) fn advance( .map_err(|_| Error::internal("RegExp exec continuation allocation failed"))?; query.parents.push(resume); *step = crate::engine::builtins::RegExpExecStep::abstract_exec( - runtime, realm, regexp, input, + runtime, + realm, + runtime + .root_and_release_jsvalue(regexp) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_and_release_jsvalue(input) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); @@ -285,7 +308,9 @@ pub(super) fn advance( runtime, realm, &constructor, - value, + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); diff --git a/src/engine/vm/proxy_get_driver/dispatch_read.rs b/src/engine/vm/proxy_get_driver/dispatch_read.rs index 38631479..972bbad7 100644 --- a/src/engine/vm/proxy_get_driver/dispatch_read.rs +++ b/src/engine/vm/proxy_get_driver/dispatch_read.rs @@ -1,9 +1,10 @@ //! Bounded native-stack dispatch for read requests. +use super::JsValue; use super::{ Completion, DescriptorStep, DirectCallTarget, Error, NativeConversion, Next, OrdinaryRead, PreparedHas, ProxyBooleanKind, ProxyBooleanStep, ProxyGetStep, ProxyOwnStep, ProxyPrototypeKind, ProxyPrototypeStep, Query, Resume, ReturnOwner, RunningExecution, Runtime, - Step, Value, overflow, runtime_error_to_vm_error, + Step, overflow, runtime_error_to_vm_error, }; #[inline(never)] @@ -63,7 +64,14 @@ pub(super) fn prototype( unreachable!() }; *step = resume - .prototype(runtime, NativeConversion::Throw(value)) + .prototype( + runtime, + NativeConversion::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?; continue; } @@ -109,7 +117,14 @@ pub(super) fn prototype( unreachable!() }; *step = resume - .boolean(runtime, NativeConversion::Throw(value)) + .boolean( + runtime, + NativeConversion::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?; continue; } @@ -181,7 +196,14 @@ pub(super) fn attributes( unreachable!() }; *step = resume - .boolean(runtime, NativeConversion::Throw(value)) + .boolean( + runtime, + NativeConversion::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?; continue; } @@ -224,7 +246,14 @@ pub(super) fn attributes( unreachable!() }; *step = resume - .boolean(runtime, NativeConversion::Throw(value)) + .boolean( + runtime, + NativeConversion::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?; continue; } @@ -311,9 +340,15 @@ pub(super) fn get( .try_reserve(1) .map_err(|_| Error::internal("property continuation allocation failed"))?; query.parents.push(resume); - *step = DescriptorStep::start(runtime, realm, value) - .map_err(runtime_error_to_vm_error)? - .into(); + *step = DescriptorStep::start( + runtime, + realm, + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ) + .map_err(runtime_error_to_vm_error)? + .into(); continue; } Step::Converted(result) => { @@ -387,6 +422,9 @@ pub(super) fn get( let receiver = receiver.take().expect("selected Step field"); let resume = resume.take().expect("selected Step field"); + let receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; let read = runtime .prepare_ordinary_read(&object, &key, receiver) .map_err(runtime_error_to_vm_error)?; @@ -406,14 +444,16 @@ pub(super) fn get( *step = resume .resume( runtime, - Completion::Return(value.unwrap_or(Value::Undefined)), + Completion::Return(value.unwrap_or(JsValue::Undefined)), ) .map_err(runtime_error_to_vm_error)?; continue; } OrdinaryRead::Call { getter, receiver } => ( DirectCallTarget::Callable(getter), - receiver, + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, Vec::new(), resume, ), @@ -439,7 +479,13 @@ pub(super) fn get( object, key, receiver, - execution.slots.take_argument_buffer(3)?, + execution + .slots + .take_argument_buffer(3)? + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); diff --git a/src/engine/vm/proxy_get_driver/dispatch_write.rs b/src/engine/vm/proxy_get_driver/dispatch_write.rs index 74af6d29..aa3fbd47 100644 --- a/src/engine/vm/proxy_get_driver/dispatch_write.rs +++ b/src/engine/vm/proxy_get_driver/dispatch_write.rs @@ -102,7 +102,14 @@ pub(super) fn keys( unreachable!() }; *step = resume - .keys(runtime, NativeConversion::Throw(value)) + .keys( + runtime, + NativeConversion::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?; continue; } @@ -146,7 +153,13 @@ pub(super) fn keys( let resume = resume.take().expect("selected Step field"); *step = match runtime - .prepare_value_property_read_completion(realm, receiver, &key) + .prepare_value_property_read_completion( + realm, + runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + &key, + ) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(read) => Step::PreparedRead { @@ -155,7 +168,14 @@ pub(super) fn keys( resume: Some(resume), }, NativeConversion::Throw(reason) => resume - .resume(runtime, Completion::Throw(reason)) + .resume( + runtime, + Completion::Throw( + runtime + .into_jsvalue(reason) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?, }; continue; @@ -199,7 +219,11 @@ pub(super) fn set( *step = resume .set( runtime, - crate::engine::object::operations::PropertySetAction::Throw(value), + crate::engine::object::operations::PropertySetAction::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), ) .map_err(runtime_error_to_vm_error)?; continue; @@ -230,9 +254,15 @@ pub(super) fn set( .try_reserve(1) .map_err(|_| Error::internal("property continuation allocation failed"))?; query.parents.push(Resume::SetLength(resume)); - *step = crate::engine::object::ArrayLengthStep::start(runtime, Some(realm), value) - .map_err(runtime_error_to_vm_error)? - .into(); + *step = crate::engine::object::ArrayLengthStep::start( + runtime, + Some(realm), + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ) + .map_err(runtime_error_to_vm_error)? + .into(); continue; } Step::SetSpecial { @@ -248,8 +278,14 @@ pub(super) fn set( let receiver = receiver.take().expect("selected Step field"); let resume = resume.take().expect("selected Step field"); + let rooted_value = runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?; + let rooted_receiver = runtime + .root_value(&receiver) + .map_err(runtime_error_to_vm_error)?; match runtime - .prepare_typed_array_set(&object, &key, &value, &receiver) + .prepare_typed_array_set(&object, &key, &rooted_value, &rooted_receiver) .map_err(runtime_error_to_vm_error)? { None => { @@ -282,8 +318,16 @@ pub(super) fn set( *step = Step::Call { target: Some(DirectCallTarget::Callable(setter)), - receiver: Some(receiver), - arguments: Some(vec![argument]), + receiver: Some( + runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, + ), + arguments: Some(vec![ + runtime + .into_jsvalue(argument) + .map_err(runtime_error_to_vm_error)?, + ]), resume: Some(Resume::Setter), }; continue; @@ -332,7 +376,11 @@ pub(super) fn set( *step = resume .set( runtime, - crate::engine::object::operations::PropertySetAction::Throw(value), + crate::engine::object::operations::PropertySetAction::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), ) .map_err(runtime_error_to_vm_error)?; continue; @@ -347,8 +395,12 @@ pub(super) fn set( Some(realm), object, key, - value, - receiver, + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? // The budget check and parent reservation above must precede @@ -382,7 +434,11 @@ pub(super) fn set( *step = resume .set( runtime, - crate::engine::object::operations::PropertySetAction::Throw(value), + crate::engine::object::operations::PropertySetAction::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), ) .map_err(runtime_error_to_vm_error)?; continue; @@ -393,7 +449,16 @@ pub(super) fn set( .map_err(|_| Error::internal("property continuation allocation failed"))?; query.parents.push(resume); *step = crate::engine::object::ProxySetStep::start( - runtime, realm, object, key, value, receiver, + runtime, + realm, + object, + key, + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, ) .map_err(runtime_error_to_vm_error)? .into(); @@ -466,7 +531,14 @@ pub(super) fn define( unreachable!() }; *step = resume - .defined(runtime, NativeConversion::Throw(value)) + .defined( + runtime, + NativeConversion::Throw( + runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ), + ) .map_err(runtime_error_to_vm_error)?; continue; } @@ -562,6 +634,7 @@ mod local_set_tests { use super::super::Parents; use super::*; use crate::engine::api::Value; + use crate::engine::value::JsValue; use crate::engine::vm::execution::ExecutionLimits; #[test] @@ -585,8 +658,8 @@ mod local_set_tests { let mut pending = Step::Set { object: Some(array.clone()), key: Some(key.clone()), - value: Some(Value::Int(7)), - receiver: Some(Value::Object(array.clone())), + value: Some(JsValue::Int(7)), + receiver: Some(runtime.unroot_value(&Value::Object(array.clone())).unwrap()), resume: Some(Resume::RootSet), }; let mut execution = RunningExecution::new( diff --git a/src/engine/vm/proxy_get_driver/native.rs b/src/engine/vm/proxy_get_driver/native.rs index b2cb1034..f61a8a34 100644 --- a/src/engine/vm/proxy_get_driver/native.rs +++ b/src/engine/vm/proxy_get_driver/native.rs @@ -10,7 +10,7 @@ pub(super) fn finish( mut resume: Resume, result: Result, ) -> Result { - let mut output = Step::Complete(Some(Completion::Return(Value::Undefined))); + let mut output = Step::Complete(Some(Completion::Return(JsValue::Undefined))); finish_into(runtime, slots, call, &mut resume, result, &mut output)?; Ok(output) } @@ -41,13 +41,23 @@ pub(super) fn finish_result( ( super::super::call::NativeInvokeMode::Ordinary, NativeInvokeOutcome::IteratorNextRaw { value, done }, - ) => Ok(NativeInvokeOutcome::Completion(Completion::Return( - Value::Object(runtime.new_iterator_result(call.activation.realm, value, done)?), - ))), + ) => { + let value = runtime + .root_and_release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + let result = runtime.new_iterator_result(call.activation.realm, value, done)?; + Ok(NativeInvokeOutcome::Completion(Completion::Return( + JsValue::Object(result.into_handle()), + ))) + } (_, result) => Ok(result), }); + let invocation = call.invocation; let (result, arguments) = call.activation.finish_reusing(result); slots.recycle_native_argument_buffer(arguments); + invocation + .release(runtime) + .map_err(runtime_error_to_vm_error)?; result.map_err(runtime_error_to_vm_error) } @@ -70,7 +80,7 @@ fn apply_into( if matches!(resume, Resume::Identity) { *output = Step::Complete(Some(identity_completion(result)?)); } else if let Resume::IteratorNext(next) = resume { - match next.raw_completion(result) { + match next.raw_completion(runtime, result) { Ok(Ok(result)) => { *output = Step::IteratorNextComplete(Some(result)); #[cfg(feature = "profiling")] @@ -128,7 +138,9 @@ pub(super) fn begin_synchronous( target, min_readable_args, super::super::call::NativeInvocation::Call { - this_value: receiver, + this_value: runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, }, arguments, super::super::call::NativeInvokeMode::Ordinary, @@ -143,13 +155,20 @@ pub(super) fn begin_synchronous( &call.activation.arguments, )? { super::super::call::NativeInvocationAdaptation::Complete(result) => result, - super::super::call::NativeInvocationAdaptation::Invoke(invocation) => kind.start( - runtime, - native_realm, - &invocation, - &call.activation.arguments, - &call.activation.callable, - )?, + super::super::call::NativeInvocationAdaptation::Invoke(invocation) => { + // `start` only borrows the adapted invocation; release the + // duplicate this adapter owns once the step has captured its + // own edges. + let started = kind.start( + runtime, + native_realm, + &invocation, + &call.activation.arguments, + &call.activation.callable, + ); + let _ = invocation.release(runtime); + started? + } }; Ok(result) })(); @@ -163,8 +182,12 @@ pub(super) fn begin_synchronous( // This ABI can only produce a Completion. It has no raw iterator variant // and therefore requires neither the generic outcome wrapper nor an // identity resume adapter. Error capture and owner cleanup stay shared. + let invocation = call.invocation; let (result, arguments) = call.activation.finish_completion_reusing(result); slots.recycle_native_argument_buffer(arguments); + invocation + .release(runtime) + .map_err(runtime_error_to_vm_error)?; result.map_err(runtime_error_to_vm_error) } @@ -234,6 +257,14 @@ fn capture_native_step( let kind = selected .take_operation() .ok_or_else(|| Error::internal("selected replace has no continuation"))?; + let receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + let arguments = arguments + .into_iter() + .map(|argument| runtime.root_and_release_jsvalue(argument)) + .collect::, _>>() + .map_err(runtime_error_to_vm_error)?; begin_local( runtime, slots, @@ -281,7 +312,7 @@ fn capture_native_step( ) .and_then(identity_completion); records[0].step = - Step::Complete(Some(Completion::Return(Value::Undefined))); + Step::Complete(Some(Completion::Return(JsValue::Undefined))); storage.recycle_native_wait(records); let result = result?; return match resume @@ -366,7 +397,9 @@ pub(super) fn begin_local( target, min_readable_args, super::super::call::NativeInvocation::Call { - this_value: receiver, + this_value: runtime + .into_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?, }, arguments, super::super::call::NativeInvokeMode::Ordinary, @@ -388,8 +421,11 @@ pub(super) fn begin_local( super::super::call::NativeInvocationAdaptation::Complete(result) => { Ok(Some(NativeInvokeOutcome::Completion(result))) } - super::super::call::NativeInvocationAdaptation::Invoke(invocation) => kind - .start_into( + super::super::call::NativeInvocationAdaptation::Invoke(invocation) => { + // `start_into` only borrows the adapted invocation; release the + // duplicate this adapter owns once the step has captured its own + // edges. + let started = kind.start_into( runtime, native_realm, &invocation, @@ -407,8 +443,10 @@ pub(super) fn begin_local( Ok(result) => transported_completion = result, Err(error) => pending_error = Some(error), }, - ) - .map_err(runtime_error_to_vm_error), + ); + let _ = invocation.release(runtime); + started.map_err(runtime_error_to_vm_error) + } })(); let immediate = match started { Ok(Some(result)) => Some(Ok(result)), @@ -431,7 +469,7 @@ pub(super) fn begin_local( if let Some(inner) = records[0].call.take() { result = finish_result(runtime, slots, inner, result); } - records[0].step = Step::Complete(Some(Completion::Return(Value::Undefined))); + records[0].step = Step::Complete(Some(Completion::Return(JsValue::Undefined))); debug_assert!( records[0] .parents @@ -685,7 +723,7 @@ pub(super) fn begin_selected_into( }; // Known Array-next calls keep every activation and ABI check above, // but need not enter the generic native dispatcher's wide frame. - match kind { + let started = match kind { crate::engine::builtins::continuation::NativeOperation::ArrayNext => { crate::engine::builtins::continuation::start_array_next_into( runtime, @@ -702,8 +740,11 @@ pub(super) fn begin_selected_into( &call.activation.callable, &mut waiting, ), - } - .map_err(runtime_error_to_vm_error) + }; + // The adapted invocation owns a duplicated edge; the started step + // captured its own copy, so release this one. + let _ = invocation.release(runtime); + started.map_err(runtime_error_to_vm_error) } })(); let immediate = match prepared { @@ -768,7 +809,7 @@ pub(super) fn compact_array_next_into( Ok(Some(NativeInvokeOutcome::Completion(result))) } super::super::call::NativeInvocationAdaptation::Invoke(invocation) => { - crate::engine::builtins::continuation::start_array_next_into( + let started = crate::engine::builtins::continuation::start_array_next_into( runtime, realm, &invocation, @@ -776,7 +817,9 @@ pub(super) fn compact_array_next_into( *output = step.into(); waiting_written = true; }, - ) + ); + let _ = invocation.release(runtime); + started } })() .map_err(runtime_error_to_vm_error); @@ -1393,9 +1436,12 @@ mod selected_replace_local_tests { false, ) .unwrap(); - let LocalNativeResult::Complete(Completion::Throw(Value::Object(error))) = result else { + let LocalNativeResult::Complete(Completion::Throw(thrown)) = result else { panic!("expected nested budget error"); }; + let Value::Object(error) = runtime.root_and_release_jsvalue(thrown).unwrap() else { + panic!("expected native error object"); + }; // The original scheduler rejects before entering the selected native: // overflow belongs to query.realm (the outer activation), unlike an // error created after entering the foreign RegExp builtin. diff --git a/src/engine/vm/proxy_get_driver/request.rs b/src/engine/vm/proxy_get_driver/request.rs index b1808038..fa97268a 100644 --- a/src/engine/vm/proxy_get_driver/request.rs +++ b/src/engine/vm/proxy_get_driver/request.rs @@ -14,9 +14,9 @@ mod vm; use super::{ BytecodeCallRequest, CompleteOrdinaryPropertyDescriptor, Completion, DescriptorResume, - DescriptorStep, DirectCallTarget, NativeConversion, ObjectRef, OrdinaryPropertyDescriptor, - OrdinaryRead, PropertyKey, ProxyBooleanResume, ProxyBooleanStep, ProxyGetResume, ProxyGetStep, - ProxyOwnResume, ProxyOwnStep, Runtime, Value, + DescriptorStep, DirectCallTarget, JsValue, NativeConversion, ObjectRef, + OrdinaryPropertyDescriptor, OrdinaryRead, PropertyKey, ProxyBooleanResume, ProxyBooleanStep, + ProxyGetResume, ProxyGetStep, ProxyOwnResume, ProxyOwnStep, Runtime, Value, }; use crate::engine::object::operations::{ InternalDefineResult, InternalSetResult, PropertySetAction, @@ -289,7 +289,7 @@ pub(super) enum Step { resume: Option, }, IntrinsicPromiseResolve { - value: Option, + value: Option, realm: Option, resume: Option, }, @@ -299,34 +299,34 @@ pub(super) enum Step { resume: Option, }, ForInComplete { - value: Option, + value: Option, done: Option>, }, TypedIteratorMethod { - source: Option, + source: Option, resume: Option, }, TypedIteratorMethodComplete( Option>>, ), TypedCollect { - source: Option, + source: Option, method: Option, element: Option, resume: Option, }, TypedCollectComplete(Option>>), TypedCreate { - constructor: Option, + constructor: Option, length: Option, resume: Option, }, NumericComplete { - value: Option, - previous: Option>, + value: Option, + previous: Option>, }, NumericHtmlDda { - value: Option, + value: Option, resume: Option, }, TypedSpeciesView { @@ -347,7 +347,7 @@ pub(super) enum Step { resume: Option, }, Aggregate { - iterable: Option, + iterable: Option, resume: Option, }, OrdinaryPrimitive { @@ -355,7 +355,7 @@ pub(super) enum Step { hint: Option, }, ConstructorSource { - new_target: Option, + new_target: Option, resume: Option, }, ConstructorSourceComplete( @@ -378,7 +378,7 @@ pub(super) enum Step { }, OrdinaryInstance { constructor: Option, - value: Option, + value: Option, resume: Option, }, ParseIterator { @@ -386,15 +386,15 @@ pub(super) enum Step { resume: Option, }, String { - value: Option, + value: Option, resume: Option, }, ObjectTag { - receiver: Option, + receiver: Option, }, RegExpExec { - regexp: Option, - input: Option, + regexp: Option, + input: Option, resume: Option, }, IteratorCloseWithResume { @@ -410,12 +410,12 @@ pub(super) enum Step { }, ArrayPush { object: Option, - value: Option, + value: Option, resume: Option, }, IteratorNext { iterator: Option, - method: Option, + method: Option, resume: Option, }, IteratorNextComplete(Option), @@ -435,19 +435,19 @@ pub(super) enum Step { min_readable_args: Option, mode: Option, invocation: Option, - arguments: Option>, + arguments: Option>, resume: Option, }, Construct { target: Option, new_target: Option, - arguments: Option>, + arguments: Option>, resume: Option, }, ConstructProxy { target: Option, new_target: Option, - arguments: Option>, + arguments: Option>, resume: Option, }, ConstructorReady { @@ -457,7 +457,7 @@ pub(super) enum Step { resume: Option, }, Arguments { - value: Option, + value: Option, resume: Option, }, ArgumentsComplete(Option>>), @@ -478,7 +478,7 @@ pub(super) enum Step { }, KeysComplete(Option>>), ReadValue { - receiver: Option, + receiver: Option, key: Option, resume: Option, }, @@ -493,7 +493,7 @@ pub(super) enum Step { resume: Option, }, Primitive { - value: Option, + value: Option, hint: Option, resume: Option, }, @@ -517,19 +517,19 @@ pub(super) enum Step { }, Element { element: Option, - value: Option, + value: Option, resume: Option, }, ElementComplete(Option>), TypedComplete(Option>), Number { - value: Option, + value: Option, resume: Option, }, NumberComplete(Option>), LengthComplete(Option), SetLength { - value: Option, + value: Option, resume: Option, }, SetComplete(Option), @@ -541,22 +541,22 @@ pub(super) enum Step { SetSpecial { object: Option, key: Option, - value: Option, - receiver: Option, + value: Option, + receiver: Option, resume: Option, }, Set { object: Option, key: Option, - value: Option, - receiver: Option, + value: Option, + receiver: Option, resume: Option, }, SetProxy { object: Option, key: Option, - value: Option, - receiver: Option, + value: Option, + receiver: Option, resume: Option, }, Define { @@ -584,13 +584,13 @@ pub(super) enum Step { Read { object: Option, key: Option, - receiver: Option, + receiver: Option, resume: Option, }, Call { target: Option, - receiver: Option, - arguments: Option>, + receiver: Option, + arguments: Option>, resume: Option, }, Descriptor { @@ -603,7 +603,7 @@ pub(super) enum Step { resume: Option, }, Convert { - value: Option, + value: Option, resume: Option, }, } @@ -636,9 +636,9 @@ impl Resume { ) -> Result { match self { Self::RootSet => Ok(Step::Complete(Some(match set_result(action)? { - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), NativeConversion::Value(result) => { - Completion::Return(Value::Bool(matches!(result, InternalSetResult::Accepted))) + Completion::Return(JsValue::Bool(matches!(result, InternalSetResult::Accepted))) } }))), @@ -728,25 +728,26 @@ impl Resume { ) -> Result { match self { Self::RootDefine => Ok(Step::Complete(Some(match result { - NativeConversion::Throw(value) => Completion::Throw(value), - NativeConversion::Value(result) => { - Completion::Return(Value::Bool(matches!(result, InternalDefineResult::Defined))) - } + NativeConversion::Throw(value) => Completion::Throw(runtime.into_jsvalue(value)?), + NativeConversion::Value(result) => Completion::Return(JsValue::Bool(matches!( + result, + InternalDefineResult::Defined + ))), }))), Self::LiteralDefinition(resume) => resume.defined(result).map(Into::into), Self::PublicField => match Runtime::finish_public_class_field_definition(result)? { crate::engine::object::operations::PropertyDefineOutcome::Defined(true) => { - Ok(Step::Complete(Some(Completion::Return(Value::Undefined)))) + Ok(Step::Complete(Some(Completion::Return(JsValue::Undefined)))) } crate::engine::object::operations::PropertyDefineOutcome::Defined(false) => { Err(crate::engine::api::runtime_error::RuntimeError::Invariant( "public field rejected without throwing", )) } - crate::engine::object::operations::PropertyDefineOutcome::Throw(value) => { - Ok(Step::Complete(Some(Completion::Throw(value)))) - } + crate::engine::object::operations::PropertyDefineOutcome::Throw(value) => Ok( + Step::Complete(Some(Completion::Throw(runtime.into_jsvalue(value)?))), + ), }, Self::JsonParse(resume) => resume @@ -810,7 +811,7 @@ impl Resume { .finish_property_delete(result, payload.strict_delete) .map(|result| Step::Complete(Some(result))), Self::Definitions(resume) => resume.boolean(runtime, result).map(Into::into), - Self::Predicate(resume) => resume.boolean(result).map(Into::into), + Self::Predicate(resume) => resume.boolean(runtime, result).map(Into::into), Self::Keys(resume) => resume.boolean(runtime, result).map(Into::into), Self::Property(resume) => resume.boolean(runtime, result).map(Into::into), Self::BuiltinPrototype(resume) => resume.boolean(runtime, result).map(Into::into), @@ -896,7 +897,7 @@ impl Resume { Self::TypedWith(resume) => resume.resume(runtime, completion).map(Into::into), Self::Uint8Codec(resume) => resume.resume(runtime, completion).map(Into::into), Self::VmNumeric(resume) => resume - .resume(completion) + .resume(runtime, completion) .map(Into::into) .map_err(crate::engine::api::runtime_error::RuntimeError::Engine), Self::TypedSearch(resume) => resume.resume(runtime, completion).map(Into::into), @@ -922,7 +923,7 @@ impl Resume { } Self::Bind(resume) => resume.resume(runtime, completion).map(Into::into), - Self::FunctionText(resume) => resume.resume(completion).map(Into::into), + Self::FunctionText(resume) => resume.resume(runtime, completion).map(Into::into), Self::DynamicFunction(resume) => resume.resume(runtime, completion).map(Into::into), Self::JsonParse(resume) => resume.resume(runtime, completion).map(Into::into), Self::JsonStringify(resume) => resume.resume(runtime, completion).map(Into::into), @@ -1001,8 +1002,13 @@ impl Resume { Self::IteratorCreate(resume) => resume.resume(runtime, completion).map(Into::into), Self::StringValue { realm, resume } => { let result = match completion { - Completion::Return(value) => runtime.string_from_primitive(realm, &value)?, - Completion::Throw(value) => NativeConversion::Throw(value), + Completion::Return(value) => { + let value = runtime.root_and_release_jsvalue(value)?; + runtime.string_from_primitive(realm, &value)? + } + Completion::Throw(value) => { + NativeConversion::Throw(runtime.root_and_release_jsvalue(value)?) + } }; resume.string(runtime, result) } @@ -1025,7 +1031,7 @@ impl Resume { } Self::Identity => Ok(Step::Complete(Some(completion))), Self::ObjectString(resume) => resume.resume(runtime, completion).map(Into::into), - Self::Definitions(resume) => resume.read(completion).map(Into::into), + Self::Definitions(resume) => resume.read(runtime, completion).map(Into::into), Self::PredicateKey(resume) => resume.key(runtime, completion).map(Into::into), Self::Keys(resume) => resume.resume(runtime, completion).map(Into::into), Self::PropertyKey(resume) => resume.key(runtime, completion).map(Into::into), @@ -1039,11 +1045,22 @@ impl Resume { Self::Prototype(resume) => resume.resume(runtime, completion).map(Into::into), Self::PrototypeGetReply(resume) => { let result = match completion { - Completion::Return(Value::Object(object)) => { - NativeConversion::Value(Some(object)) + Completion::Return(JsValue::Object(object)) => { + match runtime.root_and_release_jsvalue(JsValue::Object(object))? { + Value::Object(object) => NativeConversion::Value(Some(object)), + _ => { + return Err( + crate::engine::api::runtime_error::RuntimeError::Invariant( + "invalid GetPrototypeOf object reply", + ), + ); + } + } + } + Completion::Return(JsValue::Null) => NativeConversion::Value(None), + Completion::Throw(value) => { + NativeConversion::Throw(runtime.root_and_release_jsvalue(value)?) } - Completion::Return(Value::Null) => NativeConversion::Value(None), - Completion::Throw(value) => NativeConversion::Throw(value), _ => { return Err(crate::engine::api::runtime_error::RuntimeError::Invariant( "invalid GetPrototypeOf reply", @@ -1054,8 +1071,10 @@ impl Resume { } Self::PrototypeSetReply(resume) => { let result = match completion { - Completion::Return(Value::Bool(value)) => NativeConversion::Value(value), - Completion::Throw(value) => NativeConversion::Throw(value), + Completion::Return(JsValue::Bool(value)) => NativeConversion::Value(value), + Completion::Throw(value) => { + NativeConversion::Throw(runtime.root_and_release_jsvalue(value)?) + } _ => { return Err(crate::engine::api::runtime_error::RuntimeError::Invariant( "invalid SetPrototypeOf reply", @@ -1068,7 +1087,9 @@ impl Resume { Self::Define(resume) => resume.resume(runtime, completion).map(Into::into), Self::Setter => Ok(Step::SetComplete(Some(match completion { Completion::Return(_) => PropertySetAction::Complete, - Completion::Throw(value) => PropertySetAction::Throw(value), + Completion::Throw(value) => { + PropertySetAction::Throw(runtime.root_and_release_jsvalue(value)?) + } }))), Self::BooleanResult { .. } => { Err(crate::engine::api::runtime_error::RuntimeError::Invariant( @@ -1126,7 +1147,7 @@ impl Resume { ), }, ), - Self::Predicate(resume) => resume.descriptor(result).map(Into::into), + Self::Predicate(resume) => resume.descriptor(runtime, result).map(Into::into), Self::Keys(resume) => resume.descriptor(runtime, result).map(Into::into), Self::Get(resume) => resume.descriptor(runtime, result).map(Into::into), Self::Property(resume) => resume.descriptor(runtime, result).map(Into::into), @@ -1229,9 +1250,9 @@ impl Resume { ) -> Result { match self { Self::ForIn(resume) => resume.prototype(runtime, result).map(Into::into), - Self::Instance(resume) => resume.prototype(result).map(Into::into), - Self::Predicate(resume) => resume.prototype(result).map(Into::into), - Self::BuiltinPrototype(resume) => resume.prototype(result).map(Into::into), + Self::Instance(resume) => resume.prototype(runtime, result).map(Into::into), + Self::Predicate(resume) => resume.prototype(runtime, result).map(Into::into), + Self::BuiltinPrototype(resume) => resume.prototype(runtime, result).map(Into::into), Self::Prototype(resume) => resume.prototype(runtime, result).map(Into::into), _ => Err(crate::engine::api::runtime_error::RuntimeError::Invariant( "prototype result has no matching continuation", @@ -1247,9 +1268,9 @@ impl Resume { result: NativeConversion, ) -> Result { match self { - Self::Definitions(resume) => resume.converted(result).map(Into::into), + Self::Definitions(resume) => resume.converted(runtime, result).map(Into::into), Self::Own(resume) => resume.converted(runtime, result).map(Into::into), - Self::Property(resume) => resume.converted(result).map(Into::into), + Self::Property(resume) => resume.converted(runtime, result).map(Into::into), _ => Err(crate::engine::api::runtime_error::RuntimeError::Invariant( "descriptor conversion has no matching operation", )), @@ -1273,10 +1294,10 @@ impl Resume { Self::JsonStringify(resume) => resume.number(runtime, result).map(Into::into), Self::TypedSort(resume) => resume.number(runtime, result).map(Into::into), - Self::Math(resume) => resume.number(result).map(Into::into), - Self::Global(resume) => resume.number(result).map(Into::into), + Self::Math(resume) => resume.number(runtime, result).map(Into::into), + Self::Global(resume) => resume.number(runtime, result).map(Into::into), Self::Numeric(resume) => resume.number(runtime, result).map(Into::into), - Self::ScalarText(resume) => resume.number(result).map(Into::into), + Self::ScalarText(resume) => resume.number(runtime, result).map(Into::into), Self::DateConstructor(resume) => resume.number(runtime, result).map(Into::into), Self::DatePrototype(resume) => resume.number(runtime, result).map(Into::into), @@ -1388,8 +1409,12 @@ impl Resume { .resume( runtime, match result { - NativeConversion::Value(value) => Completion::Return(Value::String(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => { + Completion::Return(runtime.into_jsvalue(Value::String(value))?) + } + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) + } }, ) .map(Into::into), @@ -1398,8 +1423,12 @@ impl Resume { resume @ (Self::EvalScript(_) | Self::Test262Agent(_)) => resume.resume( runtime, match result { - NativeConversion::Value(value) => Completion::Return(Value::String(value)), - NativeConversion::Throw(value) => Completion::Throw(value), + NativeConversion::Value(value) => { + Completion::Return(runtime.into_jsvalue(Value::String(value))?) + } + NativeConversion::Throw(value) => { + Completion::Throw(runtime.into_jsvalue(value)?) + } }, ), @@ -1407,8 +1436,8 @@ impl Resume { Self::RegExpIterator(resume) => resume.string(runtime, result).map(Into::into), - Self::FunctionText(resume) => resume.string(result).map(Into::into), - Self::DynamicFunction(resume) => resume.string(result).map(Into::into), + Self::FunctionText(resume) => resume.string(runtime, result).map(Into::into), + Self::DynamicFunction(resume) => resume.string(runtime, result).map(Into::into), Self::JsonParse(resume) => resume.string(runtime, result).map(Into::into), Self::JsonStringify(resume) => resume.string(runtime, result).map(Into::into), Self::JsonRaw(resume) => resume diff --git a/src/engine/vm/proxy_get_driver/request/array.rs b/src/engine/vm/proxy_get_driver/request/array.rs index 5e0d844c..42ea377a 100644 --- a/src/engine/vm/proxy_get_driver/request/array.rs +++ b/src/engine/vm/proxy_get_driver/request/array.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for array domain requests. -use super::{DirectCallTarget, Resume, Step, Value}; +use super::{DirectCallTarget, JsValue, Resume, Step}; impl From for Step { fn from(step: crate::engine::builtins::ArrayMutationStep) -> Self { @@ -29,7 +29,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayMutation(resume)), @@ -62,7 +62,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), value: Some(value), key: Some(key.clone()), @@ -93,7 +93,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayCallback(resume)), @@ -160,7 +160,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArraySpecies(resume)), @@ -194,7 +194,7 @@ impl From for Step { T::Read { mut resume } => { let (object, key) = resume.take_read(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayNext(resume)), @@ -220,7 +220,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArraySort(resume)), @@ -247,7 +247,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -277,7 +277,7 @@ impl From for Step { let arguments = resume.take_call_arguments(); Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(arguments), resume: Some(Resume::ArraySort(resume)), } @@ -295,7 +295,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayIndexed(resume)), @@ -322,7 +322,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -359,7 +359,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayReverse(resume)), @@ -386,7 +386,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -490,7 +490,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -549,7 +549,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayCopy(resume)), @@ -569,7 +569,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -600,7 +600,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayConcat(resume)), @@ -627,7 +627,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -668,7 +668,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArrayFlatten(resume)), @@ -743,7 +743,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -781,7 +781,7 @@ impl From for Step { T::Read { mut resume } => { let (object, key) = resume.take_read(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ArraySlice(resume)), @@ -797,7 +797,7 @@ impl From for Step { T::Set { mut resume } => { let (object, key, value) = resume.take_set(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), diff --git a/src/engine/vm/proxy_get_driver/request/buffer.rs b/src/engine/vm/proxy_get_driver/request/buffer.rs index 14bfcbf4..824d6678 100644 --- a/src/engine/vm/proxy_get_driver/request/buffer.rs +++ b/src/engine/vm/proxy_get_driver/request/buffer.rs @@ -1,5 +1,6 @@ //! Mechanical adapters for buffer domain requests. -use super::{DirectCallTarget, ElementStep, Resume, Step, TypedWriteStep, Value}; +use super::JsValue; +use super::{DirectCallTarget, ElementStep, Resume, Step, TypedWriteStep}; impl From for Step { fn from(step: ElementStep) -> Self { @@ -9,7 +10,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Element(resume)), @@ -105,7 +106,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::TypedSpecies(resume)), @@ -135,7 +136,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::TypedIteration(resume)), @@ -187,7 +188,7 @@ impl From for Step { resume, } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(arguments), resume: Some(Resume::TypedSort(resume)), }, @@ -209,7 +210,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::BufferConstructor(resume)), @@ -255,7 +256,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::TypedSet(resume)), @@ -405,7 +406,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::BufferSlice(resume)), @@ -455,7 +456,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Uint8Codec(resume)), @@ -492,7 +493,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::TypedCollect(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/conversion.rs b/src/engine/vm/proxy_get_driver/request/conversion.rs index a0d95e0f..21a63007 100644 --- a/src/engine/vm/proxy_get_driver/request/conversion.rs +++ b/src/engine/vm/proxy_get_driver/request/conversion.rs @@ -1,5 +1,6 @@ //! Mechanical adapters for conversion domain requests. -use super::{DirectCallTarget, NumberStep, Resume, Step, Value}; +use super::JsValue; +use super::{DirectCallTarget, NumberStep, Resume, Step}; impl From for Step { fn from(step: NumberStep) -> Self { @@ -9,7 +10,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Number(resume)), @@ -38,7 +39,7 @@ impl From for Step { PrimitiveStep::Get { mut resume } => { let (object, key) = resume.take_get(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Primitive(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/function.rs b/src/engine/vm/proxy_get_driver/request/function.rs index 7e48010e..ec16e24c 100644 --- a/src/engine/vm/proxy_get_driver/request/function.rs +++ b/src/engine/vm/proxy_get_driver/request/function.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for function domain requests. -use super::{DirectCallTarget, Resume, Step, Value}; +use super::{DirectCallTarget, JsValue, Resume, Step}; impl From for Step { fn from(step: crate::engine::builtins::ArgumentsStep) -> Self { @@ -11,7 +11,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Arguments(resume)), @@ -71,7 +71,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Instance(resume)), @@ -127,7 +127,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Bind(resume)), @@ -155,7 +155,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::FunctionText(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/iterator.rs b/src/engine/vm/proxy_get_driver/request/iterator.rs index 49018df3..039107c4 100644 --- a/src/engine/vm/proxy_get_driver/request/iterator.rs +++ b/src/engine/vm/proxy_get_driver/request/iterator.rs @@ -1,5 +1,6 @@ //! Mechanical adapters for iterator domain requests. -use super::{Completion, DirectCallTarget, Resume, Step, Value}; +use super::JsValue; +use super::{Completion, DirectCallTarget, Resume, Step}; impl From for Step { fn from(step: crate::engine::builtins::IteratorCloseStep) -> Self { @@ -11,7 +12,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::IteratorClose(resume)), @@ -22,7 +23,7 @@ impl From for Step { resume, } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Object(iterator)), + receiver: Some(JsValue::Object(iterator.into_handle())), arguments: Some(Vec::new()), resume: Some(Resume::IteratorClose(resume)), }, @@ -40,7 +41,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::IteratorNext(resume)), @@ -67,7 +68,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::IteratorConsume(resume)), @@ -87,7 +88,7 @@ impl From for Step { let arguments = resume.take_call_arguments(); Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(arguments), resume: Some(Resume::IteratorConsume(resume)), } @@ -112,7 +113,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::IteratorHelper(resume)), @@ -157,7 +158,7 @@ impl From for Step { match step { T::CloseInvalidCount { iterator, resume } => Self::IteratorCloseWithResume { iterator: Some(iterator), - completion: Some(Completion::Throw(Value::Undefined)), + completion: Some(Completion::Throw(JsValue::Undefined)), resume: Some(Resume::IteratorInvalidCount(resume)), }, @@ -167,7 +168,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::IteratorCreate(resume)), @@ -277,7 +278,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::IteratorConcat(resume)), @@ -350,7 +351,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), value: Some(value), @@ -412,7 +413,7 @@ impl From for Step { resume, } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(arguments), resume: Some(Resume::WeakComputed(resume)), }, diff --git a/src/engine/vm/proxy_get_driver/request/module.rs b/src/engine/vm/proxy_get_driver/request/module.rs index bdb7b7ba..6b49898b 100644 --- a/src/engine/vm/proxy_get_driver/request/module.rs +++ b/src/engine/vm/proxy_get_driver/request/module.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for module domain operations. -use super::{DirectCallTarget, Resume, Step, Value}; +use super::{DirectCallTarget, JsValue, Resume, Step}; use crate::engine::modules::import::ImportStep; impl From for Step { fn from(step: ImportStep) -> Self { @@ -14,7 +14,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Import(resume)), @@ -38,7 +38,7 @@ impl From for Step { resume, } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(vec![reason]), resume: Some(Resume::Import(resume)), }, @@ -71,7 +71,7 @@ impl From for Step { BodyStep::Complete(result) => Self::Complete(Some(result)), BodyStep::Call { callable, resume } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(Vec::new()), resume: Some(Resume::ModuleBody(resume)), }, @@ -94,7 +94,7 @@ impl From for Step { resume, } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(vec![value]), resume: Some(Resume::ModuleEvaluation(resume)), }, @@ -117,7 +117,7 @@ impl From for Step { resume, } => Self::Call { target: Some(DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(vec![value]), resume: Some(Resume::ModuleCallback(resume)), }, diff --git a/src/engine/vm/proxy_get_driver/request/native.rs b/src/engine/vm/proxy_get_driver/request/native.rs index 63bcfc74..624ab6fd 100644 --- a/src/engine/vm/proxy_get_driver/request/native.rs +++ b/src/engine/vm/proxy_get_driver/request/native.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for native domain requests. -use super::{Completion, Resume, Step, Value}; +use super::{Resume, Step, Value}; impl From for Step { fn from(step: crate::engine::builtins::continuation::NativeStep) -> Self { @@ -48,7 +48,7 @@ impl From for Step { source: Some(source), resume: Some(Resume::Identity), }, - input => Self::Complete(Some(Completion::Return(input))), + _ => unreachable!("global eval non-string is completed before scheduling"), }, NativeStep::JsonRaw { value, resume } => Self::String { value: Some(value), diff --git a/src/engine/vm/proxy_get_driver/request/object.rs b/src/engine/vm/proxy_get_driver/request/object.rs index 8a4e6d61..7608d4e9 100644 --- a/src/engine/vm/proxy_get_driver/request/object.rs +++ b/src/engine/vm/proxy_get_driver/request/object.rs @@ -1,7 +1,8 @@ //! Mechanical adapters for object domain requests. +use super::JsValue; use super::{ ArrayLengthStep, DescriptorStep, ProxyBooleanStep, ProxyDefineStep, ProxyGetStep, ProxyOwnStep, - ProxyPrototypeStep, ProxySetStep, Resume, SetStep, Step, Value, set_completion, + ProxyPrototypeStep, ProxySetStep, Resume, SetStep, Step, set_completion, }; impl From for Step { @@ -530,7 +531,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ProxyConstruct(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/object_builtins.rs b/src/engine/vm/proxy_get_driver/request/object_builtins.rs index ccb1c515..ef5203f7 100644 --- a/src/engine/vm/proxy_get_driver/request/object_builtins.rs +++ b/src/engine/vm/proxy_get_driver/request/object_builtins.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for object builtins domain requests. -use super::{DirectCallTarget, Resume, Step, Value}; +use super::{DirectCallTarget, JsValue, Resume, Step}; impl From for Step { fn from(step: crate::engine::builtins::BuiltinPrototypeStep) -> Self { @@ -210,7 +210,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::Definitions(resume)), @@ -355,7 +355,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::ObjectCopy(resume)), @@ -399,7 +399,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::JsonParse(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/scalar.rs b/src/engine/vm/proxy_get_driver/request/scalar.rs index 447b0779..74a4a0ab 100644 --- a/src/engine/vm/proxy_get_driver/request/scalar.rs +++ b/src/engine/vm/proxy_get_driver/request/scalar.rs @@ -1,5 +1,6 @@ //! Mechanical adapters for scalar domain requests. -use super::{DirectCallTarget, Resume, Step, ToPrimitiveHint, Value}; +use super::JsValue; +use super::{DirectCallTarget, Resume, Step, ToPrimitiveHint}; impl From for Step { fn from(step: crate::engine::builtins::MathStep) -> Self { @@ -166,7 +167,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::DatePrototype(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/string.rs b/src/engine/vm/proxy_get_driver/request/string.rs index c82cfb41..de3cd64d 100644 --- a/src/engine/vm/proxy_get_driver/request/string.rs +++ b/src/engine/vm/proxy_get_driver/request/string.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for string domain requests. -use super::{Resume, Step, Value}; +use super::{JsValue, Resume, Step}; impl From for Step { fn from(step: crate::engine::builtins::StringReplaceStep) -> Self { @@ -86,7 +86,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpPresentation(resume)), @@ -182,7 +182,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::StringSearch(resume)), @@ -209,7 +209,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::StringSplit(resume)), @@ -249,7 +249,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpConstructor(resume)), @@ -290,7 +290,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::StringProtocol(resume)), @@ -340,7 +340,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpSearch(resume)), @@ -368,7 +368,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), value: Some(value), @@ -388,7 +388,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpMatch(resume)), @@ -417,7 +417,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), value: Some(value), @@ -437,7 +437,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpMatchAll(resume)), @@ -476,7 +476,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), value: Some(value), @@ -496,7 +496,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpSplit(resume)), @@ -535,7 +535,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), value: Some(value), @@ -565,7 +565,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpSpecies(resume)), @@ -583,7 +583,7 @@ impl From for Step { let object = resume.take_read_object(); let key = resume.take_read_key(); Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::RegExpIterator(resume)), @@ -618,7 +618,7 @@ impl From for Step { let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key.clone()), value: Some(value), @@ -641,7 +641,7 @@ impl From for Step { key, resume, } => Self::Read { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), resume: Some(Resume::StringFactory(resume)), diff --git a/src/engine/vm/proxy_get_driver/request/vm.rs b/src/engine/vm/proxy_get_driver/request/vm.rs index 4a377b2c..8b8c26fa 100644 --- a/src/engine/vm/proxy_get_driver/request/vm.rs +++ b/src/engine/vm/proxy_get_driver/request/vm.rs @@ -1,5 +1,5 @@ //! Mechanical adapters for vm domain requests. -use super::{Completion, Resume, Step, Value}; +use super::{Completion, JsValue, Resume, Step}; impl From for Step { fn from(step: crate::engine::vm::environment_bindings::operation::EnvironmentStep) -> Self { @@ -40,7 +40,7 @@ impl From f let key = resume.take_set_key(); let value = resume.take_set_value(); Self::Set { - receiver: Some(Value::Object(object.clone())), + receiver: Some(JsValue::Object(object.clone().into_handle())), object: Some(object), key: Some(key), value: Some(value), @@ -128,7 +128,9 @@ impl From for super::Step { key, resume, } => Self::Read { - receiver: Some(crate::engine::value::Value::Object(object.clone())), + receiver: Some(crate::engine::value::JsValue::Object( + object.clone().into_handle(), + )), object: Some(object), key: Some(key), resume: Some(super::Resume::GeneratorPrototype(resume)), @@ -165,7 +167,7 @@ impl From for super::Step { let value = resume.take_call_value(); Self::Call { target: Some(super::DirectCallTarget::Callable(callable)), - receiver: Some(crate::engine::value::Value::Undefined), + receiver: Some(crate::engine::value::JsValue::Undefined), arguments: Some(vec![value]), resume: Some(super::Resume::Async(resume)), } @@ -193,7 +195,7 @@ impl From for super::Ste let value = resume.take_call_value(); Self::Call { target: Some(super::DirectCallTarget::Callable(callable)), - receiver: Some(Value::Undefined), + receiver: Some(JsValue::Undefined), arguments: Some(vec![value]), resume: Some(super::Resume::AsyncGenerator(resume)), } diff --git a/src/engine/vm/proxy_get_driver/storage.rs b/src/engine/vm/proxy_get_driver/storage.rs index 87aa2215..b9874a3d 100644 --- a/src/engine/vm/proxy_get_driver/storage.rs +++ b/src/engine/vm/proxy_get_driver/storage.rs @@ -103,7 +103,7 @@ impl QueryStorage { waiting.push(super::native::NativeWaitRecord { call: None, step: super::Step::Complete(Some(crate::engine::vm::Completion::Return( - crate::engine::value::Value::Undefined, + crate::engine::value::JsValue::Undefined, ))), parents: Vec::new(), }); diff --git a/src/engine/vm/published_execution_tests.rs b/src/engine/vm/published_execution_tests.rs index 043884ce..46b915f6 100644 --- a/src/engine/vm/published_execution_tests.rs +++ b/src/engine/vm/published_execution_tests.rs @@ -2,7 +2,7 @@ //! access-mode checks move to publication. These tests use the real compiler, //! publisher and runtime host, not synthetic instruction fixtures. use crate::engine::api::runtime::Runtime; -use crate::engine::value::Value; +use crate::engine::value::{JsValue, Value}; #[test] fn invalid_binding_modes_are_rejected_before_creating_a_runtime_frame() { @@ -222,6 +222,7 @@ fn stack_reads_cover_full_depth_range_and_preserve_root_ownership() { let mut slots = SlotStore::new(length + 1); let mut window = slots .push_frame( + &runtime, &code.frame_layout(), FrameStorage { original_arguments: vec![], @@ -232,17 +233,17 @@ fn stack_reads_cover_full_depth_range_and_preserve_root_ownership() { ) .unwrap(); for index in 0..length { - slots.push(&mut window, Value::Int(index as i32)).unwrap(); + slots.push(&mut window, JsValue::Int(index as i32)).unwrap(); } for depth in 0..=255 { let value = slots.peek(&window, depth); if depth < length { - assert_eq!(value.unwrap(), &Value::Int((length - depth - 1) as i32)); + assert_eq!(value.unwrap(), &JsValue::Int((length - depth - 1) as i32)); } else { assert!(value.is_err()); } } - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } let object = context.new_object().unwrap(); let id = object.object_id(); @@ -251,6 +252,7 @@ fn stack_reads_cover_full_depth_range_and_preserve_root_ownership() { let mut slots = SlotStore::new(2); let mut window = slots .push_frame( + &runtime, &code.frame_layout(), FrameStorage { original_arguments: vec![], @@ -260,12 +262,19 @@ fn stack_reads_cover_full_depth_range_and_preserve_root_ownership() { }, ) .unwrap(); - slots.push(&mut window, Value::Object(object)).unwrap(); - slots.push(&mut window, Value::Int(9)).unwrap(); - let saved = slots.peek(&window, 1).unwrap().clone(); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(9)); - slots.clear_frame(window).unwrap(); + slots + .push( + &mut window, + runtime.into_jsvalue(Value::Object(object)).unwrap(), + ) + .unwrap(); + slots.push(&mut window, JsValue::Int(9)).unwrap(); + let saved = runtime + .dup_jsvalue(slots.peek(&window, 1).unwrap()) + .unwrap(); + assert_eq!(slots.pop(&mut window).unwrap(), JsValue::Int(9)); + slots.clear_frame(&runtime, window).unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_ok()); - drop(saved); + runtime.release_jsvalue(saved).unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_err()); } diff --git a/src/engine/vm/pure_operations.rs b/src/engine/vm/pure_operations.rs index ccad3a38..200ffbf6 100644 --- a/src/engine/vm/pure_operations.rs +++ b/src/engine/vm/pure_operations.rs @@ -7,24 +7,58 @@ use crate::engine::{ code::runtime::PublishedFunctionSnapshot, heap::ContextId, heap::{BytecodeConstant, ObjectPayload}, - value::{JsString, Value}, + value::{JsString, JsValue, Value}, vm::{Completion, exception::runtime_error_to_vm_error}, }; +fn allocate_string_node(runtime: &Runtime, string: JsString) -> Result { + runtime + .unroot_value(&Value::String(string)) + .map_err(runtime_error_to_vm_error) +} + +fn value_is_html_dda(runtime: &Runtime, value: &JsValue) -> Result { + if !matches!(value, JsValue::Object(_)) { + return Ok(false); + } + let rooted = runtime + .root_value(value) + .map_err(runtime_error_to_vm_error)?; + runtime + .value_is_html_dda(&rooted) + .map_err(runtime_error_to_vm_error) +} + +fn value_is_callable(runtime: &Runtime, value: &JsValue) -> Result { + if !matches!(value, JsValue::Object(_)) { + return Ok(false); + } + let rooted = runtime + .root_value(value) + .map_err(runtime_error_to_vm_error)?; + runtime + .value_is_callable(&rooted) + .map_err(runtime_error_to_vm_error) +} + /// Load a published value constant while its executable owns the raw edge. /// Template objects and Symbols need the same checked retain as the old host. pub(super) fn load_value_constant( runtime: &Runtime, executable: &PublishedFunctionSnapshot, index: u32, -) -> Result { +) -> Result { let constant = executable .constant(index) .ok_or_else(|| Error::internal("constant index is out of bounds"))?; match constant { - BytecodeConstant::Value(value) => runtime - .root_raw_value(value) - .map_err(|error| Error::internal(error.to_string())), + BytecodeConstant::Value(value) => { + let value = JsValue::from_raw(value.clone()) + .ok_or_else(|| Error::internal("constant sentinel escaped"))?; + runtime + .dup_jsvalue(&value) + .map_err(runtime_error_to_vm_error) + } BytecodeConstant::Function(_) => Err(Error::internal( "child function bytecode was loaded with a value-constant opcode", )), @@ -50,17 +84,14 @@ fn canonical_typeof_string(runtime: &Runtime, spelling: &'static str) -> Result< .map_err(|error| runtime_error_to_vm_error(error.into())) } -pub(super) fn type_of(runtime: &Runtime, value: &Value) -> Result { - let Value::Object(object) = value else { +pub(super) fn type_of(runtime: &Runtime, value: &JsValue) -> Result { + let JsValue::Object(object) = value else { return canonical_typeof_string(runtime, value.type_of()); }; - if !object.belongs_to(runtime) { - return Err(Error::internal("typeof operand belongs to another runtime")); - } let state = runtime.0.state.borrow(); let object = state .heap - .object(object.object_id()) + .object(*object) .map_err(|error| Error::internal(error.to_string()))?; if object.is_html_dda { drop(state); @@ -125,36 +156,45 @@ pub(super) fn create_regexp( } None => return Err(Error::internal("constant index is out of bounds")), }; - runtime + let object = runtime .new_compiled_regexp_literal(realm, pattern, program) - .map(|object| Completion::Return(Value::Object(object))) - .map_err(runtime_error_to_vm_error) + .map_err(runtime_error_to_vm_error)?; + let id = object.object_id(); + runtime + .retain_object_handle(id) + .map_err(|error| runtime_error_to_vm_error(error.into()))?; + Ok(Completion::Return(JsValue::Object(id))) } pub(super) fn set_object_prototype( runtime: &Runtime, - object: Value, - prototype: Value, + object: JsValue, + prototype: JsValue, ) -> Result { - let Value::Object(object) = object else { + let JsValue::Object(object) = object else { return Err(Error::internal( "object-literal prototype target was not an Object", )); }; let prototype = match prototype { - Value::Object(prototype) => Some(prototype), - Value::Null => None, + JsValue::Object(prototype) => Some( + crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), prototype) + .map_err(|error| runtime_error_to_vm_error(error.into()))?, + ), + JsValue::Null => None, // Pinned QuickJS `OP_set_proto` consumes every primitive without // changing the fresh literal. - _ => return Ok(Completion::Return(Value::Undefined)), + _ => return Ok(Completion::Return(JsValue::Undefined)), }; + let object = crate::engine::object::ObjectRef::from_borrowed_handle(runtime.clone(), object) + .map_err(|error| runtime_error_to_vm_error(error.into()))?; let changed = runtime .set_prototype_of(&object, prototype.as_ref()) .map_err(runtime_error_to_vm_error)?; if !changed { return Err(Error::new(ErrorKind::Type, "prototype is immutable")); } - Ok(Completion::Return(Value::Undefined)) + Ok(Completion::Return(JsValue::Undefined)) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -211,7 +251,7 @@ pub(super) fn step( return Err(error); }; let value = runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?; Ok(CallStep::Complete(Completion::Throw(value))) } @@ -233,17 +273,23 @@ fn perform( #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_storage( crate::engine::api::profiling::OwnedStorageEvent::Copy { - heap_root: matches!(value, Value::Object(_) | Value::Symbol(_)), + heap_root: matches!(value, JsValue::Object(_) | JsValue::Symbol(_)), }, ); value } P::IteratorCheckObject => { - super::iterator_support::check_result_object(slots.peek(&frame.window, 0)?)?; + let value = slots.peek(&frame.window, 0)?; + let rooted = runtime + .root_value(value) + .map_err(runtime_error_to_vm_error)?; + super::iterator_support::check_result_object(&rooted)?; return Ok(None); } P::IteratorMissingThrow => return Err(super::iterator_support::missing_throw()), - P::AtomValue(value) => Value::String(JsString::from_fresh_decimal_u32(value)), + P::AtomValue(value) => { + allocate_string_node(runtime, JsString::from_fresh_decimal_u32(value))? + } P::RegExp(index) => { match create_regexp(runtime, frame.executable.realm, &frame.executable, index)? { Completion::Return(value) => value, @@ -314,7 +360,9 @@ fn perform( P::SetPrototype => { let prototype = slots.pop(&mut frame.window)?; let object = slots.pop(&mut frame.window)?; - let retained = object.clone(); + let retained = runtime + .dup_jsvalue(&object) + .map_err(runtime_error_to_vm_error)?; match set_object_prototype(runtime, object, prototype)? { Completion::Return(_) => retained, Completion::Throw(_) => { @@ -327,7 +375,10 @@ fn perform( P::Branch { target, when } => { let value = slots.pop(&mut frame.window)?; let truthy = runtime - .value_to_boolean(&value) + .value_to_boolean_jsvalue(&value) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(value) .map_err(runtime_error_to_vm_error)?; return Ok((truthy == when).then_some(target as usize)); } @@ -338,29 +389,25 @@ fn perform( | P::TypeOfIsUndefined | P::TypeOfIsFunction => { let value = slots.pop(&mut frame.window)?; - match operation { - P::TypeOf => Value::String(type_of(runtime, &value)?), + let result = match operation { + P::TypeOf => allocate_string_node(runtime, type_of(runtime, &value)?)?, P::IsUndefinedOrNull => { - Value::Bool(matches!(value, Value::Null | Value::Undefined)) + JsValue::Bool(matches!(value, JsValue::Null | JsValue::Undefined)) } - P::IsUndefined => Value::Bool(matches!(value, Value::Undefined)), - P::IsNull => Value::Bool(matches!(value, Value::Null)), - P::TypeOfIsUndefined => Value::Bool( - matches!(value, Value::Undefined) - || runtime - .value_is_html_dda(&value) - .map_err(runtime_error_to_vm_error)?, + P::IsUndefined => JsValue::Bool(matches!(value, JsValue::Undefined)), + P::IsNull => JsValue::Bool(matches!(value, JsValue::Null)), + P::TypeOfIsUndefined => JsValue::Bool( + matches!(value, JsValue::Undefined) || value_is_html_dda(runtime, &value)?, ), - P::TypeOfIsFunction => Value::Bool( - !runtime - .value_is_html_dda(&value) - .map_err(runtime_error_to_vm_error)? - && runtime - .value_is_callable(&value) - .map_err(runtime_error_to_vm_error)?, + P::TypeOfIsFunction => JsValue::Bool( + !value_is_html_dda(runtime, &value)? && value_is_callable(runtime, &value)?, ), _ => unreachable!(), - } + }; + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + result } }; slots.push(&mut frame.window, result)?; diff --git a/src/engine/vm/root_call.rs b/src/engine/vm/root_call.rs index d7be9caa..49c4c787 100644 --- a/src/engine/vm/root_call.rs +++ b/src/engine/vm/root_call.rs @@ -2,7 +2,7 @@ use super::{ Completion, frame::{FrameCold, FrameEntry}, - stack::{FrameStorage, copy_value}, + stack::FrameStorage, }; use crate::engine::api::{Error, runtime::Runtime, runtime_error::RuntimeError}; use crate::engine::code::function::metadata::FunctionKind; @@ -39,7 +39,11 @@ impl Runtime { closure_slots, )?; let metadata = entry.executable.metadata; - let module_link = metadata.is_module && entry.cold.input.this_value == Value::Bool(true); + let module_link = metadata.is_module + && matches!( + entry.cold.input.this_value, + crate::engine::value::JsValue::Bool(true) + ); if metadata.function_kind == FunctionKind::Async && !module_link { return self.start_async_bytecode_callable(caller_realm, entry); } @@ -86,8 +90,12 @@ pub(in crate::engine::vm) fn prepare_call( closure_slots: crate::engine::vm::closure::ClosureSlots, ) -> Result { use crate::engine::api::runtime_error::RuntimeError; - let prepared = - runtime.prepare_owned_bytecode_frame(callable, receiver, new_target, bytecode)?; + let prepared = runtime.prepare_owned_bytecode_frame( + callable, + runtime.into_jsvalue(receiver)?, + runtime.into_jsvalue(new_target)?, + bytecode, + )?; if closure_slots.len() != usize::from(prepared.executable.metadata.closure_count) { return Err(RuntimeError::Engine(Error::internal( "function object closure slot count does not match bytecode metadata", @@ -102,7 +110,7 @@ pub(in crate::engine::vm) fn prepare_call( )) })?; for value in arguments { - original_arguments.push(copy_value(value).map_err(RuntimeError::Engine)?); + original_arguments.push(runtime.unroot_value(value)?); } let local_count = if prepared.executable.has_captured_locals { prepared.executable.local_definitions.len() diff --git a/src/engine/vm/run.rs b/src/engine/vm/run.rs index 88a4604e..bb631a4e 100644 --- a/src/engine/vm/run.rs +++ b/src/engine/vm/run.rs @@ -4,10 +4,10 @@ use crate::engine::api::error::Error; use crate::engine::code::bytecode::Instruction; use crate::engine::heap::{BytecodeConstant, RawValue, SlotReleaseReadiness}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::value::number::operations::Number; use crate::engine::vm::bindings::FrameBinding; -use crate::engine::vm::exception::runtime_error_to_vm_error; +use crate::engine::vm::exception::{heap_error_to_vm_error, runtime_error_to_vm_error}; use crate::engine::vm::execution::RunningExecution; use crate::engine::vm::frame::FrameId; use crate::engine::vm::stack::{RunSlots, copy_value}; @@ -202,7 +202,7 @@ pub(super) fn test_complete_numeric( realm: crate::engine::heap::ContextId, transaction: &mut super::stack::FrameTransaction<'_>, kind: super::numeric::operation::NumericKind, - thrown: &mut Option, + thrown: &mut Option, active_frame: super::frames::ActiveFrameToken, fault_pc: usize, ) -> Result { @@ -217,21 +217,61 @@ pub(super) fn test_complete_numeric( ) } -fn number(value: &Value) -> Option { +/// Values surrendered by the run loop's explicit outside-borrow releases. +trait ReleaseDropped { + fn release_dropped(self, runtime: &crate::engine::api::runtime::Runtime) -> Result<(), Error>; +} + +impl ReleaseDropped for JsValue { + fn release_dropped(self, runtime: &crate::engine::api::runtime::Runtime) -> Result<(), Error> { + runtime + .release_jsvalue(self) + .map_err(runtime_error_to_vm_error) + } +} + +impl ReleaseDropped for FrameBinding { + fn release_dropped(self, runtime: &crate::engine::api::runtime::Runtime) -> Result<(), Error> { + super::bindings::release_frame_binding(runtime, self) + } +} + +impl ReleaseDropped for (JsValue, JsValue) { + fn release_dropped(self, runtime: &crate::engine::api::runtime::Runtime) -> Result<(), Error> { + runtime + .release_jsvalue(self.0) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(self.1) + .map_err(runtime_error_to_vm_error) + } +} + +fn release_dropped( + runtime: &crate::engine::api::runtime::Runtime, + dropped: impl ReleaseDropped, +) -> Result<(), Error> { + dropped.release_dropped(runtime) +} + +fn number(value: &JsValue) -> Option { value.as_number_repr() } -fn value(number: Number) -> Value { - number.into() +fn value(number: Number) -> JsValue { + match number { + Number::Int(value) => JsValue::Int(value), + Number::Float(value) => JsValue::Float(value), + } } -fn immediate(value: &Value) -> bool { +fn immediate(value: &JsValue) -> bool { matches!( value, - Value::Undefined | Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) + JsValue::Undefined | JsValue::Null | JsValue::Bool(_) | JsValue::Int(_) | JsValue::Float(_) ) } fn binary( slots: &mut RunSlots<'_>, - operation: impl FnOnce(Number, Number) -> Value, + operation: impl FnOnce(Number, Number) -> JsValue, ) -> Result { slots.binary_number(operation) } @@ -248,7 +288,7 @@ fn release_displaced( // Between the initial proof and this commit, only moves and possibly one // retain occurred. Neither can invalidate the no-drain proof. if !runtime - .try_release_slot_value(&mut old) + .try_release_slot_value_jsvalue(&mut old) .map_err(runtime_error_to_vm_error)? { return Err(cold::internal( @@ -263,8 +303,8 @@ fn release_displaced( /// so the overwrite/drop paths may release them inside the RunSlots borrow /// without materialization or active-PC publication. Symbols stay conservative /// because their atom release touches runtime tables. -fn primitive_release_owner(value: &Value) -> bool { - !matches!(value, Value::Object(_) | Value::Symbol(_)) +fn primitive_release_owner(value: &JsValue) -> bool { + immediate(value) } // Explicit drops end the NoJs slot borrow before publication or owner release. @@ -293,7 +333,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result Result Result Result { - slots.push(copy_value(&cold.input.new_target)?)?; + slots.push(copy_value(runtime, &cold.input.new_target)?)?; true } Instruction::InitializeDerivedLocal(index) => { @@ -550,14 +602,18 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result return Ok(RunExit::ReturnDerived(*index)), - Instruction::CheckCtor if !matches!(cold.input.new_target, Value::Undefined) => true, + Instruction::CheckCtor if !matches!(cold.input.new_target, JsValue::Undefined) => true, Instruction::CheckCtor => { return Ok(RunExit::Pure( super::pure_operations::PureOperation::ConstructorWithoutNew, )); } Instruction::PushActiveFunction => { - slots.push(Value::Object(cold.function.clone()))?; + let id = cold.function.object_id(); + runtime + .retain_object_handle(id) + .map_err(heap_error_to_vm_error)?; + slots.push(JsValue::Object(id))?; #[cfg(feature = "profiling")] cold::storage(crate::engine::api::profiling::OwnedStorageEvent::Copy { heap_root: true, @@ -631,9 +687,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result Result { - if matches!(slots.peek(0)?, Value::Object(_)) { + if matches!(slots.peek(0)?, JsValue::Object(_)) { true } else { return Ok(RunExit::Environment( @@ -865,8 +919,8 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result match slots.peek(0)? { - Value::Int(_) | Value::String(_) => true, - Value::Symbol(symbol) if symbol.belongs_to(runtime) => true, + JsValue::Int(_) | JsValue::String(_) => true, + JsValue::Symbol(_) => true, _ => return Ok(RunExit::ConvertPropertyKey), }, Instruction::DefineFieldComputed => { @@ -990,23 +1044,23 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result true, Instruction::PushI32(number) => { - slots.push(Value::Int(*number))?; + slots.push(JsValue::Int(*number))?; true } Instruction::Undefined => { - slots.push(Value::Undefined)?; + slots.push(JsValue::Undefined)?; true } Instruction::Null => { - slots.push(Value::Null)?; + slots.push(JsValue::Null)?; true } Instruction::PushTrue => { - slots.push(Value::Bool(true))?; + slots.push(JsValue::Bool(true))?; true } Instruction::PushFalse => { - slots.push(Value::Bool(false))?; + slots.push(JsValue::Bool(false))?; true } Instruction::PushConst(index) => { @@ -1024,24 +1078,30 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { - Some(Value::Int(*number)) + Some(JsValue::Int(*number)) } Some(BytecodeConstant::Value(RawValue::Float(number))) => { - Some(Value::Float(*number)) + Some(JsValue::Float(*number)) } - Some(BytecodeConstant::Value(RawValue::Undefined)) => Some(Value::Undefined), - Some(BytecodeConstant::Value(RawValue::Null)) => Some(Value::Null), + Some(BytecodeConstant::Value(RawValue::Undefined)) => Some(JsValue::Undefined), + Some(BytecodeConstant::Value(RawValue::Null)) => Some(JsValue::Null), Some(BytecodeConstant::Value(RawValue::Bool(value))) => { - Some(Value::Bool(*value)) - } - Some(BytecodeConstant::Value(RawValue::String(value))) => { - Some(Value::String(value.clone())) - } - Some(BytecodeConstant::Value(RawValue::BigInt(value))) => { - Some(Value::BigInt(value.clone())) + Some(JsValue::Bool(*value)) } + Some(BytecodeConstant::Value(RawValue::String(value))) => Some( + runtime + .dup_jsvalue(&JsValue::String(*value)) + .map_err(runtime_error_to_vm_error)?, + ), + Some(BytecodeConstant::Value(RawValue::BigInt(value))) => Some( + runtime + .dup_jsvalue(&JsValue::BigInt(*value)) + .map_err(runtime_error_to_vm_error)?, + ), _ => None, }; if let Some(value) = result { @@ -1061,9 +1121,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result Result { - super::bindings::read_run_cell(runtime, &root)? + super::bindings::read_run_cell(runtime, &root) } _ => None, } @@ -1263,13 +1321,13 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { - let copied = copy_value(value)?; + let copied = copy_value(runtime, value)?; slots.push(copied)?; true } FrameBinding::Captured(root) => { if let Some((value, _owned)) = - super::bindings::read_run_cell(runtime, &root)? + super::bindings::read_run_cell(runtime, &root) { slots.push(value)?; #[cfg(feature = "profiling")] @@ -1361,7 +1419,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result true, FrameBinding::Direct(old) => { runtime - .slot_value_release_readiness(old) + .slot_value_release_readiness_jsvalue(old) .map_err(runtime_error_to_vm_error)? == SlotReleaseReadiness::Ready } @@ -1405,7 +1463,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result true, FrameBinding::Direct(old) => { runtime - .slot_value_release_readiness(old) + .slot_value_release_readiness_jsvalue(old) .map_err(runtime_error_to_vm_error)? == SlotReleaseReadiness::Ready } @@ -1442,13 +1500,13 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { - if matches!(slots.local(*index)?, FrameBinding::Direct(old) if runtime.slot_value_release_readiness(old).map_err(runtime_error_to_vm_error)? == SlotReleaseReadiness::Ready) + if matches!(slots.local(*index)?, FrameBinding::Direct(old) if runtime.slot_value_release_readiness_jsvalue(old).map_err(runtime_error_to_vm_error)? == SlotReleaseReadiness::Ready) { let next = if matches!( instruction, Instruction::SetLocal(_) | Instruction::SetLocalCheck(_) ) { - copy_value(slots.peek(0)?)? + copy_value(runtime, slots.peek(0)?)? } else { slots.pop()? }; @@ -1461,7 +1519,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result Result Result { if let FrameBinding::Direct(value) = slots.parameter(*index)? { - let copied = copy_value(value)?; + let copied = copy_value(runtime, value)?; slots.push(copied)?; true } else { @@ -1495,10 +1553,10 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { - if matches!(slots.parameter(*index)?, FrameBinding::Direct(old) if runtime.slot_value_release_readiness(old).map_err(runtime_error_to_vm_error)? == SlotReleaseReadiness::Ready) + if matches!(slots.parameter(*index)?, FrameBinding::Direct(old) if runtime.slot_value_release_readiness_jsvalue(old).map_err(runtime_error_to_vm_error)? == SlotReleaseReadiness::Ready) { let next = if matches!(instruction, Instruction::SetArg(_)) { - copy_value(slots.peek(0)?)? + copy_value(runtime, slots.peek(0)?)? } else { slots.pop()? }; @@ -1508,7 +1566,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result Result Result { - slots.insert_copy(0, 0)?; + slots.insert_copy(runtime, 0, 0)?; true } Instruction::Dup1 => { - slots.insert_copy(1, 1)?; + slots.insert_copy(runtime, 1, 1)?; true } Instruction::Dup3 => { - slots.duplicate_operands(3)?; + slots.duplicate_operands(runtime, 3)?; true } Instruction::Insert2 | Instruction::Insert3 | Instruction::Insert4 => { @@ -1548,7 +1606,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result 4, }; slots.peek(count - 1)?; - slots.insert_copy(0, count)?; + slots.insert_copy(runtime, 0, count)?; true } Instruction::Perm3 | Instruction::Perm4 | Instruction::Perm5 => { @@ -1570,7 +1628,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result Result Result binary(&mut slots, |a, b| value(a.rem(b)))?, Instruction::Pow => binary(&mut slots, |a, b| value(a.pow(b)))?, Instruction::Shl => binary(&mut slots, |a, b| { - Value::Int(a.int32().wrapping_shl(b.int32() as u32 & 31)) + JsValue::Int(a.int32().wrapping_shl(b.int32() as u32 & 31)) })?, Instruction::Sar => binary(&mut slots, |a, b| { - Value::Int(a.int32() >> (b.int32() as u32 & 31)) + JsValue::Int(a.int32() >> (b.int32() as u32 & 31)) })?, Instruction::Shr => binary(&mut slots, |a, b| { value(Number::compact(f64::from( (a.int32() as u32) >> (b.int32() as u32 & 31), ))) })?, - Instruction::BitAnd => binary(&mut slots, |a, b| Value::Int(a.int32() & b.int32()))?, - Instruction::BitOr => binary(&mut slots, |a, b| Value::Int(a.int32() | b.int32()))?, - Instruction::BitXor => binary(&mut slots, |a, b| Value::Int(a.int32() ^ b.int32()))?, + Instruction::BitAnd => binary(&mut slots, |a, b| JsValue::Int(a.int32() & b.int32()))?, + Instruction::BitOr => binary(&mut slots, |a, b| JsValue::Int(a.int32() | b.int32()))?, + Instruction::BitXor => binary(&mut slots, |a, b| JsValue::Int(a.int32() ^ b.int32()))?, Instruction::Lt | Instruction::Lte | Instruction::Gt @@ -1661,62 +1719,80 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result binary(&mut slots, |a, b| Value::Bool(a.float() < b.float()))?, - Instruction::Lte => binary(&mut slots, |a, b| Value::Bool(a.float() <= b.float()))?, - Instruction::Gt => binary(&mut slots, |a, b| Value::Bool(a.float() > b.float()))?, - Instruction::Gte => binary(&mut slots, |a, b| Value::Bool(a.float() >= b.float()))?, + Instruction::Lt => binary(&mut slots, |a, b| JsValue::Bool(a.float() < b.float()))?, + Instruction::Lte => binary(&mut slots, |a, b| JsValue::Bool(a.float() <= b.float()))?, + Instruction::Gt => binary(&mut slots, |a, b| JsValue::Bool(a.float() > b.float()))?, + Instruction::Gte => binary(&mut slots, |a, b| JsValue::Bool(a.float() >= b.float()))?, Instruction::StrictEq | Instruction::StrictNeq => { let negate = matches!(instruction, Instruction::StrictNeq); if binary(&mut slots, |a, b| { - Value::Bool((a.float() == b.float()) != negate) + JsValue::Bool((a.float() == b.float()) != negate) })? { true } else { - if matches!((slots.peek(1)?, slots.peek(0)?), - (Value::String(left), Value::String(right)) if !left.is_flat() || !right.is_flat()) { - return Ok(RunExit::StrictEquality(negate)); + // Rope (non-flat) spellings take the resident string + // comparison; the arena dereference is pure. + let rope = match (slots.peek(1)?, slots.peek(0)?) { + (JsValue::String(left), JsValue::String(right)) => { + let state = runtime.0.state.borrow(); + let heap = &state.heap; + let left = heap.string_fast(*left); + let right = heap.string_fast(*right); + !left.is_flat() || !right.is_flat() + } + _ => false, + }; + if rope { + return Ok(RunExit::StrictEquality(negate)); + } } - let equal = slots.peek(1)?.strict_equal(slots.peek(0)?) != negate; + let equal = runtime + .strict_equal_jsvalue(slots.peek(1)?, slots.peek(0)?) + .map_err(runtime_error_to_vm_error)? + != negate; let observable = (0..2).any(|offset| { - matches!(slots.peek(offset), Ok(Value::Object(_) | Value::Symbol(_))) + matches!( + slots.peek(offset), + Ok(JsValue::Object(_) | JsValue::Symbol(_)) + ) }); if observable { release_outside_slots!({ let right = slots.pop()?; let left = slots.pop()?; - slots.push(Value::Bool(equal))?; + slots.push(JsValue::Bool(equal))?; (left, right) }); } else { let right = slots.pop()?; let left = slots.pop()?; - slots.push(Value::Bool(equal))?; + slots.push(JsValue::Bool(equal))?; drop(slots); - drop((left, right)); + release_dropped(runtime, (left, right))?; slots = transaction.slots(); } true } } - Instruction::Eq => binary(&mut slots, |a, b| Value::Bool(a.float() == b.float()))?, - Instruction::Neq => binary(&mut slots, |a, b| Value::Bool(a.float() != b.float()))?, + Instruction::Eq => binary(&mut slots, |a, b| JsValue::Bool(a.float() == b.float()))?, + Instruction::Neq => binary(&mut slots, |a, b| JsValue::Bool(a.float() != b.float()))?, Instruction::Not => { // Includes Annex B HTMLDDA objects; metadata lookup cannot run JS. let result = !runtime - .value_to_boolean(slots.peek(0)?) + .value_to_boolean_jsvalue(slots.peek(0)?) .map_err(runtime_error_to_vm_error)?; - if matches!(slots.peek(0)?, Value::Object(_) | Value::Symbol(_)) { + if matches!(slots.peek(0)?, JsValue::Object(_) | JsValue::Symbol(_)) { release_outside_slots!({ let input = slots.pop()?; - slots.push(Value::Bool(result))?; + slots.push(JsValue::Bool(result))?; input }); } else { let input = slots.pop()?; - slots.push(Value::Bool(result))?; + slots.push(JsValue::Bool(result))?; drop(slots); - drop(input); + release_dropped(runtime, input)?; slots = transaction.slots(); } true @@ -1777,12 +1853,12 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { let pc = i32::try_from(next_pc) .map_err(|_| cold::internal("gosub return PC does not fit Int"))?; - slots.push(Value::Int(pc))?; + slots.push(JsValue::Int(pc))?; next_pc = *target as usize; true } Instruction::Ret => { - let Value::Int(target) = slots.pop()? else { + let JsValue::Int(target) = slots.pop()? else { return Err(cold::internal("invalid ret value")); }; next_pc = @@ -1793,7 +1869,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { - if !matches!(slots.pop()?, Value::Int(_)) { + if !matches!(slots.pop()?, JsValue::Int(_)) { return Err(cold::internal("invalid gosub cleanup value")); } true @@ -1827,7 +1903,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result { - execution.pending = Some(Value::Undefined); + execution.pending = Some(JsValue::Undefined); pc.resume = next_pc; #[cfg(feature = "profiling")] cold::instruction(observed_depth); @@ -1842,7 +1918,7 @@ pub(super) fn run(execution: &mut RunningExecution, id: FrameId) -> Result, kind: NumericKind) -> bool { kind.primitive_arithmetic() && (0..if kind.unary() { 1 } else { 2 }).all( - |offset| matches!(slots.peek(offset), Ok(value) if !matches!(value, Value::Object(_))), + |offset| matches!(slots.peek(offset), Ok(value) if !matches!(value, JsValue::Object(_))), ) } @@ -32,7 +32,7 @@ pub(super) fn complete( realm: ContextId, transaction: &mut FrameTransaction<'_>, kind: NumericKind, - thrown: &mut Option, + thrown: &mut Option, active_frame: super::super::frames::ActiveFrameToken, fault_pc: usize, ) -> Result { @@ -47,7 +47,7 @@ pub(super) fn complete( }; // Parsing, BigInt allocation, Symbol release and error materialization all // occur after the input RunSlots has ended. Object coercion is never admitted. - let output = match primitive_output(kind, left, right) { + let output = match primitive_output(runtime, kind, left, right) { Ok(output) => output, Err(error) => { let Some(kind) = @@ -68,7 +68,7 @@ pub(super) fn complete( } *thrown = Some( runtime - .new_native_error_from_error(realm, kind, &error) + .new_native_error_from_error_jsvalue(realm, kind, &error) .map_err(runtime_error_to_vm_error)?, ); return Ok(false); diff --git a/src/engine/vm/run/property.rs b/src/engine/vm/run/property.rs index fcbb0f0c..d0429ced 100644 --- a/src/engine/vm/run/property.rs +++ b/src/engine/vm/run/property.rs @@ -2,7 +2,7 @@ use crate::engine::{ api::{Error, runtime::Runtime}, code::runtime::PublishedFunctionSnapshot, - value::Value, + value::JsValue, vm::{exception::runtime_error_to_vm_error, stack::FrameTransaction}, }; #[derive(Clone, Copy)] @@ -13,10 +13,13 @@ pub(super) enum Operation { Define(u32), Delete, } -fn index(value: &Value) -> Option { +fn index(runtime: &Runtime, value: &JsValue) -> Option { match value { - Value::Int(n) => u32::try_from(*n).ok(), - Value::String(s) => crate::engine::atom::AtomTable::canonical_array_index(s), + JsValue::Int(n) => u32::try_from(*n).ok(), + JsValue::String(id) => { + let text = super::super::numeric::string_payload(runtime, *id).ok()?; + crate::engine::atom::AtomTable::canonical_array_index(&text) + } _ => None, } } @@ -35,7 +38,7 @@ pub(super) fn complete( let key = slots.pop()?; (slots.pop()?, key, value) }; - let handled = match index(&key) { + let handled = match index(runtime, &key) { Some(index) => runtime .try_dense_array_write_owned(&base, index, &value) .map_err(runtime_error_to_vm_error)?, @@ -46,6 +49,18 @@ pub(super) fn complete( slots.push(base)?; slots.push(key)?; slots.push(value)?; + } else { + // The stored slot retains its own edge; the consumed operands must + // still be released or their roots leak. + runtime + .release_jsvalue(key) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; } #[cfg(feature = "profiling")] if handled { @@ -69,18 +84,21 @@ pub(super) fn complete( .try_define_field_owned(&base, executable, key, &value) .map_err(runtime_error_to_vm_error)?, Operation::Delete => { - if matches!(value, Value::Int(_) | Value::String(_) | Value::Symbol(_)) { + if matches!( + value, + JsValue::Int(_) | JsValue::String(_) | JsValue::Symbol(_) + ) { let key = super::super::property_keys::canonical(runtime, &value)?; result = runtime .try_delete_own_data(&base, &key) .map_err(runtime_error_to_vm_error)? - .map(Value::Bool); + .map(JsValue::Bool); } result.is_some() } Operation::ElementRead(_) => { - result = - index(&value).and_then(|index| runtime.try_dense_array_kept_read(&base, index)); + result = index(runtime, &value) + .and_then(|index| runtime.try_dense_array_kept_read(&base, index)); result.is_some() } Operation::ElementWrite => unreachable!(), @@ -92,17 +110,42 @@ pub(super) fn complete( return Ok(false); } match operation { - Operation::Define(_) => transaction.slots().push(base)?, + Operation::Define(_) => { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + transaction.slots().push(base)?; + } Operation::ElementRead(keep_key) => { let mut slots = transaction.slots(); slots.push(base)?; if keep_key { slots.push(value)?; + } else { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; } slots.push(result.expect("read result"))?; } - Operation::Delete => transaction.slots().push(result.expect("delete result"))?, - Operation::Write(_) | Operation::ElementWrite => {} + Operation::Delete => { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; + transaction.slots().push(result.expect("delete result"))?; + } + Operation::Write(_) => { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(base) + .map_err(runtime_error_to_vm_error)?; + } + Operation::ElementWrite => unreachable!(), } #[cfg(feature = "profiling")] crate::engine::api::profiling::record_owned_execution_event(match operation { diff --git a/src/engine/vm/stack.rs b/src/engine/vm/stack.rs index 702d158a..6b00a49e 100644 --- a/src/engine/vm/stack.rs +++ b/src/engine/vm/stack.rs @@ -9,8 +9,8 @@ use crate::engine::api::error::Error; use crate::engine::api::profiling::{OwnedStorageEvent as Cost, record_owned_storage}; use crate::engine::api::runtime::Runtime; use crate::engine::code::function::layout::FrameLayout; -use crate::engine::value::Value; -use crate::engine::vm::bindings::FrameBinding; +use crate::engine::value::{JsValue, Value}; +use crate::engine::vm::bindings::{FrameBinding, release_frame_binding as release_binding}; use crate::engine::vm::exception::runtime_error_to_vm_error; use std::ops::Range; use std::rc::Rc; @@ -21,14 +21,21 @@ pub(in crate::engine::vm) struct SlotStore { // active_end participates in frame authority and the logical slot limit. slots: Vec>, active_end: usize, - argument_buffer: Vec, - native_argument_buffers: Vec>, + // Outgoing synchronous call argument buffer (internal values). + argument_buffer: Vec, + // Native readable-argument buffers: internal values recycled across + // activations; the public boundary converts at the call site. + native_argument_buffers: Vec>, owner: Rc<()>, next_window: u64, windows: Vec, limit: usize, #[cfg(feature = "profiling")] live_slots: usize, + // Scratch carrier for the take-frame profiling omission count between the + // span move and its completion; never observed across operations. + #[cfg(feature = "profiling")] + omitted: usize, } /// Not Clone: releasing a frame consumes its authority over the window. @@ -74,10 +81,99 @@ impl FrameWindow { } pub(in crate::engine::vm) struct FrameStorage { - pub original_arguments: Vec, + pub original_arguments: Vec, pub parameters: Vec, pub locals: Vec, - pub operands: Vec, + pub operands: Vec, +} + +/// Release every edge still owned by an abandoned `FrameStorage`. +/// +/// Error and abandonment paths call this before dropping the storage so no +/// internal `JsValue`/`FrameBinding` edge survives without its owner. Releases +/// are defer-safe and nothrow, and never run JavaScript. +pub(in crate::engine::vm) fn release_frame_storage(runtime: &Runtime, storage: FrameStorage) { + for value in storage.original_arguments { + let _ = runtime.release_jsvalue(value); + } + for binding in storage.parameters { + let _ = release_binding(runtime, binding); + } + for binding in storage.locals { + let _ = release_binding(runtime, binding); + } + for value in storage.operands { + let _ = runtime.release_jsvalue(value); + } +} + +/// Release only the object/symbol edges an abandoned frame storage still owns, +/// plus every non-direct binding edge. Direct String/BigInt edges are the +/// boundary-conversion producer edges the caller already released through the +/// encoded activation, so they are deliberately skipped to avoid a double +/// release. Releases are defer-safe and nothrow. +pub(in crate::engine::vm) fn release_unconverted_frame_storage( + runtime: &Runtime, + storage: FrameStorage, +) { + for value in storage + .original_arguments + .into_iter() + .chain(storage.operands) + { + if matches!(value, JsValue::Object(_) | JsValue::Symbol(_)) { + let _ = runtime.release_jsvalue(value); + } + } + for binding in storage.parameters.into_iter().chain(storage.locals) { + match binding { + FrameBinding::Direct(value) => { + if matches!(value, JsValue::Object(_) | JsValue::Symbol(_)) { + let _ = runtime.release_jsvalue(value); + } + } + other => { + let _ = release_binding(runtime, other); + } + } + } +} + +/// Owns a `FrameStorage` across fallible migration and releases every still +/// owned edge if the migration is abandoned. `take` hands the storage back for +/// the success path, after which this guard performs no release. +pub(in crate::engine::vm) struct FrameStorageGuard { + runtime: Runtime, + storage: Option, +} + +impl FrameStorageGuard { + pub(in crate::engine::vm) fn new(runtime: &Runtime, storage: FrameStorage) -> Self { + Self { + runtime: runtime.clone(), + storage: Some(storage), + } + } + + pub(in crate::engine::vm) fn storage_mut(&mut self) -> &mut FrameStorage { + self.storage + .as_mut() + .expect("frame storage already surrendered") + } + + pub(in crate::engine::vm) fn take(&mut self) -> FrameStorage { + self.storage + .take() + .expect("frame storage already surrendered") + } +} + +impl Drop for FrameStorageGuard { + fn drop(&mut self) { + if let Some(storage) = self.storage.take() { + release_frame_storage(&self.runtime, storage); + } + } } mod number; @@ -110,11 +206,31 @@ impl SlotStore { None }; let base = self.peek_current(window, 0)?; - let Some(value) = runtime - .try_property_ic_read_owned(base, executable, pc, key_index, keep_receiver, native) - .map_err(crate::engine::vm::exception::runtime_error_to_vm_error)? - else { - return Ok(false); + let value = match runtime.property_ic_read_fast( + base, + executable, + pc, + key_index, + keep_receiver, + native, + ) { + Some(value) => value, + None => { + let Some(value) = runtime + .try_property_ic_read_owned( + base, + executable, + pc, + key_index, + keep_receiver, + native, + ) + .map_err(crate::engine::vm::exception::runtime_error_to_vm_error)? + else { + return Ok(false); + }; + value + } }; if let Some(index) = output_index { self.install_operand(window, index, value); @@ -122,8 +238,12 @@ impl SlotStore { let index = window.operands().start + window.depth - 1; let base = self.slots[index].replace(FrameBinding::Direct(value)); // No reentry or cleanup queue mutation intervenes between the - // runtime proof and this non-final receiver decrement. - drop(base); + // runtime proof and this non-final receiver release. + if let Some(FrameBinding::Direct(old)) = base { + runtime + .release_jsvalue(old) + .map_err(runtime_error_to_vm_error)?; + } #[cfg(feature = "profiling")] record_owned_storage(Cost::Move(2)); } @@ -142,6 +262,8 @@ impl SlotStore { limit, #[cfg(feature = "profiling")] live_slots: 0, + #[cfg(feature = "profiling")] + omitted: 0, } } @@ -168,10 +290,11 @@ impl SlotStore { Ok(()) } + #[cfg_attr(not(test), allow(dead_code))] pub(in crate::engine::vm) fn take_native_argument_buffer( &mut self, count: usize, - ) -> Result, Error> { + ) -> Result, Error> { let mut arguments = self.native_argument_buffers.pop().unwrap_or_default(); debug_assert!(arguments.is_empty()); let _before = arguments.capacity(); @@ -194,21 +317,26 @@ impl SlotStore { /// moves occur until the original callee-release boundary. pub(in crate::engine::vm) fn take_native_call_operands( &mut self, + runtime: &Runtime, window: &mut FrameWindow, count: usize, method: bool, ) -> Result<(Vec, Value), Error> { self.check_current(window)?; - self.take_native_call_operands_current(window, count, method) + self.take_native_call_operands_current(runtime, window, count, method) } fn take_native_call_operands_current( &mut self, + runtime: &Runtime, window: &mut FrameWindow, count: usize, method: bool, ) -> Result<(Vec, Value), Error> { - let mut arguments = self.take_native_argument_buffer(count)?; + let mut arguments: Vec = Vec::new(); + arguments + .try_reserve_exact(count) + .map_err(|_| Error::internal("native call arguments allocation failed"))?; for offset in 0..count + 1 + usize::from(method) { self.peek_current(window, offset)?; } @@ -217,7 +345,16 @@ impl SlotStore { let Some(FrameBinding::Direct(value)) = self.slots[index].take() else { unreachable!("native operand transaction authenticated each slot") }; - arguments.push(value); + // Native dispatch consumes public roots; root the moved owner at + // this boundary and release its internal edge. + arguments.push( + runtime + .root_value(&value) + .map_err(runtime_error_to_vm_error)?, + ); + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; } window.depth -= count; #[cfg(feature = "profiling")] @@ -231,9 +368,19 @@ impl SlotStore { } // Match the previous callee then receiver pop/drop order. The classified // callable owner pins the callee throughout this transfer. - drop(self.pop_current(window)?); + let callee = self.pop_current(window)?; + runtime + .release_jsvalue(callee) + .map_err(runtime_error_to_vm_error)?; let receiver = if method { - self.pop_current(window)? + let receiver = self.pop_current(window)?; + let rooted = runtime + .root_value(&receiver) + .map_err(runtime_error_to_vm_error)?; + runtime + .release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; + rooted } else { Value::Undefined }; @@ -242,7 +389,10 @@ impl SlotStore { /// Cleanup cannot allocate or retain JavaScript owners. Producers outside /// the native caller path may supply extra buffers; discard excess capacity. - pub(in crate::engine::vm) fn recycle_native_argument_buffer(&mut self, arguments: Vec) { + pub(in crate::engine::vm) fn recycle_native_argument_buffer( + &mut self, + arguments: Vec, + ) { debug_assert!(arguments.is_empty()); if !arguments.is_empty() { return; @@ -257,7 +407,7 @@ impl SlotStore { pub(in crate::engine::vm) fn take_argument_buffer( &mut self, count: usize, - ) -> Result, Error> { + ) -> Result, Error> { let mut arguments = std::mem::take(&mut self.argument_buffer); debug_assert!(arguments.is_empty()); let before = arguments.capacity(); @@ -293,27 +443,37 @@ impl SlotStore { /// snapshot remains a separate range even for strict/non-simple parameters. pub(in crate::engine::vm) fn push_frame( &mut self, + runtime: &Runtime, layout: &FrameLayout<'_>, storage: FrameStorage, ) -> Result { - self.push_frame_storage(layout, storage, None, None) + self.push_frame_storage(runtime, layout, storage, None, None) } pub(in crate::engine::vm) fn push_initialized_frame( &mut self, + runtime: &Runtime, layout: &FrameLayout<'_>, storage: FrameStorage, function: &crate::engine::object::ObjectRef, function_name: Option, ) -> Result { - self.push_frame_storage(layout, storage, Some((function, function_name)), None) + self.push_frame_storage( + runtime, + layout, + storage, + Some((function, function_name)), + None, + ) } /// Reserve and copy the writable parameter snapshot before consuming any /// caller owner. Originals then move straight from the outgoing operand /// tail into the callee snapshot; the caller prefix never moves. + #[allow(clippy::too_many_arguments)] pub(in crate::engine::vm) fn push_call_frame( &mut self, + runtime: &Runtime, layout: &FrameLayout<'_>, parent: &mut FrameWindow, count: usize, @@ -330,6 +490,7 @@ impl SlotStore { self.peek_current(parent, offset)?; } self.push_frame_storage( + runtime, layout, FrameStorage { original_arguments: Vec::new(), @@ -342,8 +503,10 @@ impl SlotStore { ) } + #[allow(clippy::too_many_arguments)] fn push_frame_storage( &mut self, + runtime: &Runtime, layout: &FrameLayout<'_>, mut storage: FrameStorage, initialize: Option<(&crate::engine::object::ObjectRef, Option)>, @@ -363,6 +526,7 @@ impl SlotStore { || !storage.locals.is_empty() || !storage.operands.is_empty()) { + release_frame_storage(runtime, storage); return Err(Error::internal( "fresh frame already contains initialized bindings", )); @@ -370,41 +534,52 @@ impl SlotStore { if initialize .is_some_and(|(_, name)| name.is_some_and(|index| usize::from(index) >= local_count)) { + release_frame_storage(runtime, storage); return Err(Error::internal("function-name local is outside the frame")); } if (!fresh && (storage.parameters.len() != parameter_count || storage.locals.len() != local_count)) || storage.operands.len() > layout.operand_capacity() { + release_frame_storage(runtime, storage); return Err(Error::internal( "owned frame storage disagrees with its published layout", )); } - let next_window = self - .next_window - .checked_add(1) - .ok_or_else(|| Error::internal("frame window identity exhausted"))?; + let Some(next_window) = self.next_window.checked_add(1) else { + release_frame_storage(runtime, storage); + return Err(Error::internal("frame window identity exhausted")); + }; let base = self.active_end; let original_end = base.checked_add(actual_count); let parameters_end = original_end.and_then(|n| n.checked_add(parameter_count)); let locals_end = parameters_end.and_then(|n| n.checked_add(local_count)); - let end = locals_end + let Some(end) = locals_end .and_then(|n| n.checked_add(layout.operand_capacity())) .filter(|end| *end <= self.limit) - .ok_or_else(|| Error::internal("execution slot limit exceeded"))?; + else { + release_frame_storage(runtime, storage); + return Err(Error::internal("execution slot limit exceeded")); + }; #[cfg(feature = "profiling")] let capacity_before = self.slots.capacity(); - self.slots + if self + .slots .try_reserve(end.saturating_sub(self.slots.len())) - .map_err(|_| Error::internal("execution slot allocation failed"))?; + .is_err() + { + release_frame_storage(runtime, storage); + return Err(Error::internal("execution slot allocation failed")); + } #[cfg(feature = "profiling")] record_owned_storage(Cost::SlotCapacity { before: capacity_before, after: self.slots.capacity(), }); - self.windows - .try_reserve(1) - .map_err(|_| Error::internal("execution window allocation failed"))?; + if self.windows.try_reserve(1).is_err() { + release_frame_storage(runtime, storage); + return Err(Error::internal("execution window allocation failed")); + } let original_end = original_end.unwrap(); let parameters_end = parameters_end.unwrap(); let locals_end = locals_end.unwrap(); @@ -430,7 +605,8 @@ impl SlotStore { for index in 0..actual_count { let value = if let Some(start) = source_start { let Some(FrameBinding::Direct(value)) = &self.slots[start + index] else { - self.clear_unpublished(original_end..original_end + index); + self.clear_unpublished(runtime, original_end..original_end + index)?; + release_frame_storage(runtime, storage); return Err(Error::internal("outgoing argument is not a direct owner")); }; value @@ -440,28 +616,43 @@ impl SlotStore { #[cfg(feature = "profiling")] { root_copies += - usize::from(matches!(value, Value::Object(_) | Value::Symbol(_))); + usize::from(matches!(value, JsValue::Object(_) | JsValue::Symbol(_))); } - match copy_value(value) { + match runtime + .dup_jsvalue(value) + .map_err(runtime_error_to_vm_error) + { Ok(value) => { self.slots[original_end + index] = Some(FrameBinding::Direct(value)) } Err(error) => { - self.clear_unpublished(original_end..original_end + index); + self.clear_unpublished(runtime, original_end..original_end + index)?; + release_frame_storage(runtime, storage); return Err(error); } } } for index in original_end + actual_count..parameters_end { - self.slots[index] = Some(FrameBinding::Direct(Value::Undefined)); + self.slots[index] = Some(FrameBinding::Direct(JsValue::Undefined)); } for (index, definition) in layout.locals().iter().enumerate() { - self.slots[parameters_end + index] = - Some(super::call::prepare::initial_local_binding( - definition.is_lexical, - function_name == Some(index as u16), - function, - )); + let binding = match super::call::prepare::initial_local_binding( + runtime, + definition.is_lexical, + function_name == Some(index as u16), + function, + ) { + Ok(binding) => binding, + Err(error) => { + self.clear_unpublished( + runtime, + original_end..original_end + actual_count + parameter_count + index, + )?; + release_frame_storage(runtime, storage); + return Err(runtime_error_to_vm_error(error)); + } + }; + self.slots[parameters_end + index] = Some(binding); } #[cfg(feature = "profiling")] crate::engine::api::profiling::record_call_preparation( @@ -480,7 +671,9 @@ impl SlotStore { } let consumed = count + 1 + usize::from(method); for index in start - 1 - usize::from(method)..start { - self.slots[index].take(); + if let Some(binding) = self.slots[index].take() { + release_binding(runtime, binding)?; + } } parent.depth -= consumed; #[cfg(feature = "profiling")] @@ -543,10 +736,13 @@ impl SlotStore { // Only fallible parameter copies can reach this unpublished rollback. // Keep the backing initialized while releasing staged owners in index order. - fn clear_unpublished(&mut self, range: Range) { + fn clear_unpublished(&mut self, runtime: &Runtime, range: Range) -> Result<(), Error> { for index in range { - self.slots[index].take(); + if let Some(binding) = self.slots[index].take() { + release_binding(runtime, binding)?; + } } + Ok(()) } #[cfg(feature = "profiling")] @@ -579,13 +775,13 @@ impl SlotStore { &self, window: &FrameWindow, from_top: usize, - ) -> Result<&Value, Error> { + ) -> Result<&JsValue, Error> { self.check_current(window)?; self.peek_current(window, from_top) } #[inline] - fn peek_current(&self, window: &FrameWindow, from_top: usize) -> Result<&Value, Error> { + fn peek_current(&self, window: &FrameWindow, from_top: usize) -> Result<&JsValue, Error> { let offset = from_top .checked_add(1) .and_then(|offset| window.depth.checked_sub(offset)) @@ -624,23 +820,20 @@ impl SlotStore { count: usize, method: bool, ) -> Result { + // Internal values carry no runtime branding: every operand domain is + // inherently local. The peek order still authenticates each slot in + // the original receiver/left-to-right rejection order. + let _ = runtime; if method { - runtime - .validate_value_domain( - self.peek_current( - window, - count - .checked_add(1) - .ok_or_else(|| Error::internal("owned operand stack underflow"))?, - )?, - "call this value", - ) - .map_err(runtime_error_to_vm_error)?; + self.peek_current( + window, + count + .checked_add(1) + .ok_or_else(|| Error::internal("owned operand stack underflow"))?, + )?; } for offset in (0..count).rev() { - runtime - .validate_value_domain(self.peek_current(window, offset)?, "call argument") - .map_err(runtime_error_to_vm_error)?; + self.peek_current(window, offset)?; } Ok(true) } @@ -654,7 +847,7 @@ impl SlotStore { operation: impl FnOnce( crate::engine::value::number::operations::Number, crate::engine::value::number::operations::Number, - ) -> Value, + ) -> JsValue, ) -> Result { self.check_current(window)?; self.binary_number_current(window, operation) @@ -667,7 +860,7 @@ impl SlotStore { operation: impl FnOnce( crate::engine::value::number::operations::Number, crate::engine::value::number::operations::Number, - ) -> Value, + ) -> JsValue, ) -> Result { let offset = window .depth @@ -716,17 +909,19 @@ impl SlotStore { else { return Err(Error::internal("owned operand slot is not a value")); }; - let Value::Int(key) = key else { + let JsValue::Int(key) = key else { return Ok(false); }; if *key < 0 { return Ok(false); } let typed = match value { - Value::Int(value) => { + JsValue::Int(value) => { runtime.try_typed_array_number_write(base, *key as u32, f64::from(*value)) } - Value::Float(value) => runtime.try_typed_array_number_write(base, *key as u32, *value), + JsValue::Float(value) => { + runtime.try_typed_array_number_write(base, *key as u32, *value) + } _ => false, }; if !typed @@ -737,14 +932,14 @@ impl SlotStore { return Ok(false); } // The successful leaf proved base's sole release cannot drain. Only - // numeric input moves occur before its Drop; no proof can change. + // numeric input moves occur before its release; no proof can change. let value = self.slots[index + 2].take(); let key = self.slots[index + 1].take(); let base = self.slots[index].take(); window.depth = offset; - drop(value); - drop(key); - drop(base); + for binding in [value, key, base].into_iter().flatten() { + release_binding(runtime, binding)?; + } #[cfg(feature = "profiling")] { self.live_slots -= 3; @@ -776,9 +971,31 @@ impl SlotStore { return Err(Error::internal("owned operand slot is not a value")); }; let index_key = match key { - Value::Int(key) if *key >= 0 => *key as u32, - Value::String(key) if key.release_keeps_storage_alive() => { - let Some(index) = crate::engine::atom::AtomTable::canonical_array_index(key) else { + JsValue::Int(key) if *key >= 0 => *key as u32, + JsValue::String(id) => { + // Mirror the historical guard: only a key whose node survives + // its own release takes this leaf. Shared payload storage means + // dropping the arena edge cannot destroy the spelling; otherwise + // fall back to the arena slot readiness proof. The spelling is + // cloned out before the slot owners are consumed below. + let content_survives = runtime + .0 + .state + .borrow() + .heap + .string_fast(*id) + .release_keeps_storage_alive(); + if !content_survives + && !matches!( + runtime.slot_value_release_readiness_jsvalue(key), + Ok(crate::engine::heap::SlotReleaseReadiness::Ready) + ) + { + return Ok(false); + } + let text = runtime.0.state.borrow().heap.string_fast(*id).clone(); + let Some(index) = crate::engine::atom::AtomTable::canonical_array_index(&text) + else { return Ok(false); }; index @@ -788,13 +1005,14 @@ impl SlotStore { let Some(value) = runtime.try_array_immediate_read(base, index_key) else { return Ok(false); }; - // The scalar result owns no heap root. Preflight proved that releasing + // The scalar result owns no heap edge. Preflight proved that releasing // the base cannot drain; no ownership decrease intervened since then. let key = self.slots[index + 1].take(); let base = self.slots[index].replace(FrameBinding::Direct(value)); window.depth -= 1; - drop(key); - drop(base); + for binding in [key, base].into_iter().flatten() { + release_binding(runtime, binding)?; + } #[cfg(feature = "profiling")] { self.live_slots -= 1; @@ -822,7 +1040,9 @@ impl SlotStore { // proof and replacing this already-validated top operand. let index = window.operands().start + window.depth - 1; let base = self.slots[index].replace(FrameBinding::Direct(value)); - drop(base); + if let Some(binding) = base { + release_binding(runtime, binding)?; + } #[cfg(feature = "profiling")] { record_owned_storage(Cost::Move(2)); @@ -862,7 +1082,9 @@ impl SlotStore { let base = self.slots[index].take(); let value = self.slots[index + 1].take(); window.depth = offset; - drop((base, value)); + for binding in [base, value].into_iter().flatten() { + release_binding(runtime, binding)?; + } #[cfg(feature = "profiling")] { self.live_slots -= 2; @@ -875,14 +1097,14 @@ impl SlotStore { pub(in crate::engine::vm) fn push( &mut self, window: &mut FrameWindow, - value: Value, + value: JsValue, ) -> Result<(), Error> { self.check_current(window)?; self.push_current(window, value) } #[inline] - fn push_current(&mut self, window: &mut FrameWindow, value: Value) -> Result<(), Error> { + fn push_current(&mut self, window: &mut FrameWindow, value: JsValue) -> Result<(), Error> { let index = self.operand_push_index(window)?; self.install_operand(window, index, value); Ok(()) @@ -892,7 +1114,7 @@ impl SlotStore { fn push_pending_current( &mut self, window: &mut FrameWindow, - value: &mut Option, + value: &mut Option, ) -> Result<(), Error> { let index = self.operand_push_index(window)?; self.install_operand(window, index, value.take().expect("pending operand owner")); @@ -919,7 +1141,7 @@ impl SlotStore { /// The checked index is private and consumed without an observable boundary. #[inline] - fn install_operand(&mut self, window: &mut FrameWindow, index: usize, value: Value) { + fn install_operand(&mut self, window: &mut FrameWindow, index: usize, value: JsValue) { self.slots[index] = Some(FrameBinding::Direct(value)); window.depth += 1; #[cfg(feature = "profiling")] @@ -974,16 +1196,18 @@ impl SlotStore { #[cfg(test)] pub(in crate::engine::vm) fn insert_copy( &mut self, + runtime: &Runtime, window: &mut FrameWindow, source_from_top: usize, destination_from_top: usize, ) -> Result<(), Error> { self.check_current(window)?; - self.insert_copy_current(window, source_from_top, destination_from_top) + self.insert_copy_current(runtime, window, source_from_top, destination_from_top) } fn insert_copy_current( &mut self, + runtime: &Runtime, window: &mut FrameWindow, source_from_top: usize, destination_from_top: usize, @@ -992,7 +1216,7 @@ impl SlotStore { if destination_from_top > window.depth || window.depth >= window.operands().len() { return Err(Error::internal("owned insertion exceeds verified capacity")); } - let copied = copy_value(self.peek_current(window, source_from_top)?)?; + let copied = copy_value(runtime, self.peek_current(window, source_from_top)?)?; self.push_current(window, copied)?; self.rotate_operands_current(window, 0, destination_from_top + 1, false) } @@ -1004,15 +1228,17 @@ impl SlotStore { #[cfg(test)] pub(in crate::engine::vm) fn duplicate_operands( &mut self, + runtime: &Runtime, window: &mut FrameWindow, count: usize, ) -> Result<(), Error> { self.check_current(window)?; - self.duplicate_operands_current(window, count) + self.duplicate_operands_current(runtime, window, count) } fn duplicate_operands_current( &mut self, + runtime: &Runtime, window: &mut FrameWindow, count: usize, ) -> Result<(), Error> { @@ -1027,20 +1253,23 @@ impl SlotStore { } for _ in 0..count { // As depth grows, this fixed offset visits the next original slot. - let copied = copy_value(self.peek_current(window, source)?)?; + let copied = copy_value(runtime, self.peek_current(window, source)?)?; self.push_current(window, copied)?; } Ok(()) } /// Logical pop removes ownership immediately. No dead value survives above sp. - pub(in crate::engine::vm) fn pop(&mut self, window: &mut FrameWindow) -> Result { + pub(in crate::engine::vm) fn pop( + &mut self, + window: &mut FrameWindow, + ) -> Result { self.check_current(window)?; self.pop_current(window) } #[inline] - fn pop_current(&mut self, window: &mut FrameWindow) -> Result { + fn pop_current(&mut self, window: &mut FrameWindow) -> Result { self.peek_current(window, 0)?; window.depth -= 1; #[cfg(feature = "profiling")] @@ -1057,12 +1286,13 @@ impl SlotStore { } /// Replace an authenticated live operand without changing its depth or neighbors. + /// The displaced owner is returned; the caller releases or moves it. pub(in crate::engine::vm) fn replace_operand( &mut self, window: &FrameWindow, from_top: usize, - value: Value, - ) -> Result { + value: JsValue, + ) -> Result { self.peek(window, from_top)?; let index = window.operands().start + window.depth - from_top - 1; #[cfg(feature = "profiling")] @@ -1100,7 +1330,7 @@ impl SlotStore { unreachable!() }; runtime - .try_release_slot_value(value) + .try_release_slot_value_jsvalue(value) .map_err(runtime_error_to_vm_error) } @@ -1199,7 +1429,7 @@ impl SlotStore { &self, window: &FrameWindow, runtime: &crate::engine::api::runtime::Runtime, - ) -> Result, Error> { + ) -> Result, Error> { self.snapshot_argument_tail(window, runtime, 0) } @@ -1216,7 +1446,7 @@ impl SlotStore { window: &FrameWindow, runtime: &Runtime, start: usize, - ) -> Result, Error> { + ) -> Result, Error> { self.check_current(window)?; let count = window.actual_count; if count > window.parameters().len() || start > window.parameters().len() { @@ -1288,69 +1518,129 @@ impl SlotStore { /// the caller receives the very owners which occupied this window. pub(in crate::engine::vm) fn take_frame( &mut self, + runtime: &Runtime, window: FrameWindow, ) -> Result { self.check_current(&window)?; + let taken = self.take_frame_owners(runtime, &window)?; + self.complete_take_frame(window, taken) + } + + fn take_frame_owners( + &mut self, + runtime: &Runtime, + window: &FrameWindow, + ) -> Result { + let mut taken = FrameStorage { + original_arguments: Vec::new(), + parameters: Vec::new(), + locals: Vec::new(), + operands: Vec::new(), + }; + let result = self.take_frame_span(window, &mut taken); + if let Err(error) = result { + // Surrender every owner moved out before the malformed slot. + for value in taken.original_arguments.drain(..) { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + } + for binding in taken.parameters.drain(..).chain(taken.locals.drain(..)) { + release_binding(runtime, binding)?; + } + for value in taken.operands.drain(..) { + runtime + .release_jsvalue(value) + .map_err(runtime_error_to_vm_error)?; + } + return Err(error); + } + Ok(taken) + } + + fn take_frame_span( + &mut self, + window: &FrameWindow, + taken: &mut FrameStorage, + ) -> Result<(), Error> { // Reserve every destination before removing any source owner. A failed - // migration allocation leaves the complete window rooted in this store. - let mut original_arguments = Vec::new(); - let mut parameters = Vec::new(); - let mut locals = Vec::new(); - let mut operands = Vec::new(); - original_arguments + // migration allocation leaves the complete window owned by this store. + taken + .original_arguments .try_reserve_exact(window.actual_count) .map_err(|_| Error::internal("original argument handoff allocation failed"))?; - parameters + taken + .parameters .try_reserve_exact(window.parameters().len()) .map_err(|_| Error::internal("parameter handoff allocation failed"))?; - locals + taken + .locals .try_reserve_exact(window.locals().len()) .map_err(|_| Error::internal("local handoff allocation failed"))?; - operands + taken + .operands .try_reserve_exact(window.depth) .map_err(|_| Error::internal("operand handoff allocation failed"))?; for index in window.original_arguments() { let Some(FrameBinding::Direct(value)) = self.slots[index].take() else { return Err(Error::internal("original argument is not an owned value")); }; - original_arguments.push(value); + taken.original_arguments.push(value); } // Only unobservable scalar originals may be absent. Preserve arity // for explicit legacy handoff without inventing reference owners. #[cfg(feature = "profiling")] - let omitted = window.actual_count - original_arguments.len(); - original_arguments.resize(window.actual_count, Value::Undefined); + let omitted = window.actual_count - taken.original_arguments.len(); + while taken.original_arguments.len() < window.actual_count { + taken.original_arguments.push(JsValue::Undefined); + } for index in window.parameters() { - parameters.push(self.slots[index].take().unwrap()); + taken.parameters.push(self.slots[index].take().unwrap()); } for index in window.locals() { - locals.push(self.slots[index].take().unwrap()); + taken.locals.push(self.slots[index].take().unwrap()); } for index in window.operands().start..window.operands().start + window.depth { let Some(FrameBinding::Direct(value)) = self.slots[index].take() else { return Err(Error::internal("operand is not an owned value")); }; - operands.push(value); + taken.operands.push(value); } #[cfg(feature = "profiling")] { - let moved = original_arguments.len() + parameters.len() + locals.len() + operands.len(); - self.live_slots -= moved - omitted; + self.omitted = omitted; + } + Ok(()) + } + + fn complete_take_frame( + &mut self, + window: FrameWindow, + taken: FrameStorage, + ) -> Result { + #[cfg(feature = "profiling")] + { + let moved = taken.original_arguments.len() + + taken.parameters.len() + + taken.locals.len() + + taken.operands.len(); + self.live_slots -= moved - self.omitted; record_owned_storage(Cost::Move(moved)); } debug_assert!(self.slots[window.whole()].iter().all(Option::is_none)); self.active_end = window.whole().start; self.windows.pop(); - Ok(FrameStorage { - original_arguments, - parameters, - locals, - operands, - }) + Ok(taken) } - /// The driver must keep the completion/pending result rooted before clearing. - pub(in crate::engine::vm) fn clear_frame(&mut self, window: FrameWindow) -> Result<(), Error> { + /// The driver must keep the completion/pending result owned before + /// clearing. Every released binding's edges are surrendered through the + /// runtime's deferred-release path. + pub(in crate::engine::vm) fn clear_frame( + &mut self, + runtime: &Runtime, + window: FrameWindow, + ) -> Result<(), Error> { self.check_current(&window)?; #[cfg(feature = "profiling")] { @@ -1365,7 +1655,9 @@ impl SlotStore { // suffix. Preserve that authority boundary and ascending owner order. self.active_end = window.whole().start; for index in window.whole().start..window.operands().start + window.depth { - self.slots[index].take(); + if let Some(binding) = self.slots[index].take() { + release_binding(runtime, binding)?; + } } debug_assert!(self.slots[window.whole()].iter().all(Option::is_none)); self.windows.pop(); @@ -1373,20 +1665,21 @@ impl SlotStore { } } -/// The running stack's copy boundary. Object retain is fallible and neither -/// drains references nor calls JS; primitive Rc copies preserve representation. -/// Releases are separate, so a failed retain cannot repeat a committed release. -// The scalar arm is 73 bytes out of line and remains visible in call-loop -// CPU profiles. Keep only this tag/copy arm resident; reference work is outlined. +/// The running stack's copy boundary. Scalars copy inline; every heap-backed +/// kind duplicates its edge through the runtime. Releases are separate, so a +/// failed retain cannot repeat a committed release. #[inline(always)] -pub(in crate::engine::vm) fn copy_value(value: &Value) -> Result { +pub(in crate::engine::vm) fn copy_value( + runtime: &Runtime, + value: &JsValue, +) -> Result { let copied = match value { - Value::Undefined => Value::Undefined, - Value::Null => Value::Null, - Value::Bool(value) => Value::Bool(*value), - Value::Int(value) => Value::Int(*value), - Value::Float(value) => Value::Float(*value), - _ => return copy_reference(value), + JsValue::Undefined => JsValue::Undefined, + JsValue::Null => JsValue::Null, + JsValue::Bool(value) => JsValue::Bool(*value), + JsValue::Int(value) => JsValue::Int(*value), + JsValue::Float(value) => JsValue::Float(*value), + _ => return copy_reference(runtime, value), }; #[cfg(feature = "profiling")] record_copy(value); @@ -1396,40 +1689,25 @@ pub(in crate::engine::vm) fn copy_value(value: &Value) -> Result { // Keep fallible heap retains and their error formatting out of scalar copies. // This is not cold: String/BigInt copies also share this boundary. #[inline(never)] -fn copy_reference(value: &Value) -> Result { - let copied = match value { - Value::String(value) => Value::String(value.clone()), - Value::BigInt(value) => Value::BigInt(value.clone()), - Value::Object(value) => Value::Object( - value - .try_clone() - .map_err(|error| Error::internal(error.to_string()))?, - ), - Value::Symbol(value) => Value::Symbol( - value - .try_clone() - .map_err(|error| Error::internal(error.to_string()))?, - ), - Value::Undefined | Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) => { - unreachable!("scalar copy entered reference helper") - } - }; +fn copy_reference(runtime: &Runtime, value: &JsValue) -> Result { + let copied = runtime + .dup_jsvalue(value) + .map_err(|error| Error::internal(error.to_string()))?; #[cfg(feature = "profiling")] record_copy(value); Ok(copied) } #[cfg(feature = "profiling")] -fn record_copy(value: &Value) { +fn record_copy(value: &JsValue) { record_owned_storage(Cost::Copy { - heap_root: matches!(value, Value::Object(_) | Value::Symbol(_)), + heap_root: matches!(value, JsValue::Object(_) | JsValue::Symbol(_)), }); crate::engine::api::profiling::record_owned_execution_event(match value { - Value::String(_) => "slot_copy.StringRc", - Value::BigInt(value) if value.as_i64().is_some() => "slot_copy.BigIntImmediate", - Value::BigInt(_) => "slot_copy.BigIntRc", - Value::Object(_) => "slot_copy.ObjectRetain", - Value::Symbol(_) => "slot_copy.SymbolRetain", + JsValue::String(_) => "slot_copy.StringNode", + JsValue::BigInt(_) => "slot_copy.BigIntNode", + JsValue::Object(_) => "slot_copy.ObjectRetain", + JsValue::Symbol(_) => "slot_copy.SymbolRetain", _ => "slot_copy.Immediate", }); } @@ -1475,19 +1753,23 @@ mod tests { panic!("object") }; let mut native = None; + let base_internal = runtime.unroot_value(&base).unwrap(); assert!( runtime - .try_property_ic_read_owned(&base, &code, pc, key, true, &mut native) + .try_property_ic_read_owned(&base_internal, &code, pc, key, true, &mut native) .unwrap() .is_none() ); + runtime.release_jsvalue(base_internal).unwrap(); let mut owner = PublishedFunctionSnapshot::empty_for_test(context.realm); owner.metadata.max_stack = 1; let mut slots = SlotStore::new(2); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) + .unwrap(); + slots + .push(&mut window, into_internal(&runtime, base.clone())) .unwrap(); - slots.push(&mut window, base.clone()).unwrap(); let count = runtime .0 .state @@ -1513,7 +1795,7 @@ mod tests { count ); assert_eq!(window.depth, 1); - assert_eq!(slots.peek(&window, 0).unwrap(), &base); + assert_eq!(to_public(&runtime, slots.peek(&window, 0).unwrap()), base); assert!( slots .run_window(&mut window) @@ -1522,13 +1804,15 @@ mod tests { .unwrap() ); assert_eq!(window.depth, 1); - assert_eq!(slots.peek(&window, 0).unwrap(), &value); - slots.clear_frame(window).unwrap(); + assert_eq!(to_public(&runtime, slots.peek(&window, 0).unwrap()), value); + slots.clear_frame(&runtime, window).unwrap(); owner.metadata.max_stack = 2; let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) + .unwrap(); + slots + .push(&mut window, into_internal(&runtime, base.clone())) .unwrap(); - slots.push(&mut window, base.clone()).unwrap(); assert!( slots .run_window(&mut window) @@ -1537,17 +1821,32 @@ mod tests { .unwrap() ); assert_eq!(window.depth, 2); - assert_eq!(slots.peek(&window, 0).unwrap(), &value); - assert_eq!(slots.peek(&window, 1).unwrap(), &base); - slots.clear_frame(window).unwrap(); + assert_eq!(to_public(&runtime, slots.peek(&window, 0).unwrap()), value); + assert_eq!(to_public(&runtime, slots.peek(&window, 1).unwrap()), base); + slots.clear_frame(&runtime, window).unwrap(); } use super::{FrameStorage, SlotStore}; use crate::engine::api::Runtime; use crate::engine::code::runtime::PublishedFunctionSnapshot; - use crate::engine::value::Value; + use crate::engine::value::{JsValue, Value}; use crate::engine::vm::bindings::FrameBinding; + fn into_internal(runtime: &Runtime, value: Value) -> JsValue { + match value { + Value::Object(object) => JsValue::Object(object.into_handle()), + other => runtime.into_jsvalue(other).unwrap(), + } + } + + fn to_public(runtime: &Runtime, value: &JsValue) -> Value { + runtime.root_value(value).unwrap() + } + + fn take_public(runtime: &Runtime, value: JsValue) -> Value { + runtime.root_and_release_jsvalue(value).unwrap() + } + #[test] fn native_argument_transaction_preserves_order_and_surviving_owners() { let runtime = Runtime::new(); @@ -1556,7 +1855,7 @@ mod tests { owner.metadata.max_stack = 6; let mut slots = SlotStore::new(6); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let receiver = context.eval("({tag:1})").unwrap(); let argument = context.eval("({tag:2})").unwrap(); @@ -1569,7 +1868,9 @@ mod tests { argument.clone(), Value::Int(3), ] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } assert!( slots @@ -1577,15 +1878,18 @@ mod tests { .unwrap() ); let (arguments, moved_receiver) = slots - .take_native_call_operands(&mut window, 3, true) + .take_native_call_operands(&runtime, &mut window, 3, true) .unwrap(); assert_eq!(arguments, [Value::Int(1), argument, Value::Int(3)]); assert_eq!(moved_receiver, receiver); assert_eq!(slots.depth(&window), 1); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(99)); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(99) + ); drop(arguments); drop(moved_receiver); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -1630,10 +1934,14 @@ mod tests { owner.metadata.max_stack = 3; let mut slots = SlotStore::new(3); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) + .unwrap(); + slots + .push(&mut window, into_internal(&runtime, Value::Int(99))) + .unwrap(); + slots + .push(&mut window, into_internal(&runtime, base)) .unwrap(); - slots.push(&mut window, Value::Int(99)).unwrap(); - slots.push(&mut window, base).unwrap(); assert!( !slots .run_window(&mut window) @@ -1643,9 +1951,11 @@ mod tests { ); assert_eq!(window.depth, 2); assert!( - matches!(slots.peek(&window,0).unwrap(),Value::Object(root) if root.object_id()==id) + matches!(slots.peek(&window, 0).unwrap(), JsValue::Object(handle) if *handle == id) ); - slots.push(&mut window, Value::Int(17)).unwrap(); + slots + .push(&mut window, into_internal(&runtime, Value::Int(17))) + .unwrap(); assert!( !slots .run_window(&mut window) @@ -1654,106 +1964,99 @@ mod tests { .unwrap() ); assert_eq!(window.depth, 3); - assert_eq!(slots.peek(&window, 0).unwrap(), &Value::Int(17)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 0).unwrap()), + Value::Int(17) + ); assert!( - matches!(slots.peek(&window,1).unwrap(),Value::Object(root) if root.object_id()==id) + matches!(slots.peek(&window, 1).unwrap(), JsValue::Object(handle) if *handle == id) + ); + assert_eq!( + to_public(&runtime, slots.peek(&window, 2).unwrap()), + Value::Int(99) ); - assert_eq!(slots.peek(&window, 2).unwrap(), &Value::Int(99)); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } } #[test] fn call_domain_validation_preserves_receiver_then_argument_rejection_order() { - for (foreign_receiver, foreign_first, malformed_first) in [ - (true, false, true), - (false, true, false), - (false, false, true), - ] { + // Internal values carry no runtime branding, so domain validation only + // enforces receiver/left-to-right slot presence. + for removed_index in [0, 2, 3] { let runtime = Runtime::new(); - let foreign = Runtime::new(); let context = runtime.new_context(); let mut owner = PublishedFunctionSnapshot::empty_for_test(context.realm); owner.metadata.max_stack = 4; let mut slots = SlotStore::new(4); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); - let receiver = Value::Object(if foreign_receiver { - foreign.new_object(None).unwrap() - } else { - runtime.new_object(None).unwrap() - }); - let first = Value::Object(if foreign_first { - foreign.new_object(None).unwrap() - } else { - runtime.new_object(None).unwrap() - }); // Caller shape is [receiver, callee, first argument, second argument]. for value in [ - receiver, + Value::Object(runtime.new_object(None).unwrap()), Value::Int(0), - first, - Value::Object(foreign.new_object(None).unwrap()), + Value::Object(runtime.new_object(None).unwrap()), + Value::Object(runtime.new_object(None).unwrap()), ] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } - let removed_index = if malformed_first { 2 } else { 3 }; let removed = slots.slots[removed_index].take(); - let result = slots.validate_call_value_domains(&window, &runtime, 2, true); - if foreign_receiver || foreign_first { - assert!(result.is_err()); - } else { - assert!( - result - .unwrap_err() - .to_string() - .contains("owned operand slot is not a value") - ); - } + let error = slots + .validate_call_value_domains(&window, &runtime, 2, true) + .unwrap_err(); + assert!( + error + .to_string() + .contains("owned operand slot is not a value"), + "removed slot {removed_index}: {error}" + ); assert_eq!(window.depth, 4); assert!(slots.slots[removed_index].is_none()); slots.slots[removed_index] = removed; - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } } #[test] fn call_domain_validation_borrows_values_without_heap_borrow_or_owner_changes() { let runtime = Runtime::new(); - let foreign = Runtime::new(); let context = runtime.new_context(); let mut owner = PublishedFunctionSnapshot::empty_for_test(context.realm); owner.metadata.max_stack = 3; let mut slots = SlotStore::new(3); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let local = runtime.new_object(None).unwrap(); let id = local.object_id(); for value in [Value::Int(0), Value::Object(local), Value::Int(42)] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } { + // Operand domains are inherently local, so validation neither + // borrows the heap nor consults runtime branding. let state = runtime.0.state.borrow_mut(); assert!( slots .validate_call_value_domains(&window, &runtime, 2, false) .unwrap() ); - assert!( - slots - .validate_call_value_domains(&window, &foreign, 2, false) - .is_err() - ); assert!(state.heap.object(id).is_ok()); } assert_eq!(window.depth, 3); - assert_eq!(slots.peek(&window, 0).unwrap(), &Value::Int(42)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 0).unwrap()), + Value::Int(42) + ); assert!( - matches!(slots.peek(&window, 1).unwrap(), Value::Object(root) if root.object_id()==id) + matches!(slots.peek(&window, 1).unwrap(), JsValue::Object(handle) if *handle == id) ); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_err()); } @@ -1765,10 +2068,14 @@ mod tests { owner.metadata.max_stack = 2; let mut slots = SlotStore::new(4); let mut parent = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) + .unwrap(); + slots + .push(&mut parent, into_internal(&runtime, Value::Int(0))) + .unwrap(); + slots + .push(&mut parent, into_internal(&runtime, Value::Int(42))) .unwrap(); - slots.push(&mut parent, Value::Int(0)).unwrap(); - slots.push(&mut parent, Value::Int(42)).unwrap(); let other = SlotStore::new(2); assert!( other @@ -1776,7 +2083,7 @@ mod tests { .is_err() ); let child = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); assert!( slots @@ -1784,15 +2091,21 @@ mod tests { .is_err() ); assert_eq!(parent.depth, 2); - slots.clear_frame(child).unwrap(); + slots.clear_frame(&runtime, child).unwrap(); assert!( slots .validate_call_value_domains(&parent, &runtime, 1, false) .unwrap() ); - assert_eq!(slots.pop(&mut parent).unwrap(), Value::Int(42)); - assert_eq!(slots.pop(&mut parent).unwrap(), Value::Int(0)); - slots.clear_frame(parent).unwrap(); + assert_eq!( + take_public(&runtime, slots.pop(&mut parent).unwrap()), + Value::Int(42) + ); + assert_eq!( + take_public(&runtime, slots.pop(&mut parent).unwrap()), + Value::Int(0) + ); + slots.clear_frame(&runtime, parent).unwrap(); } #[test] @@ -1847,10 +2160,12 @@ mod tests { owner.metadata.max_stack = 3; let mut slots = SlotStore::new(3); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for value in [base, Value::Int(key), value] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } assert!( !slots @@ -1861,9 +2176,15 @@ mod tests { "{source}" ); assert_eq!(window.depth, 3); - assert_eq!(slots.peek(&window, 1).unwrap(), &Value::Int(key)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 1).unwrap()), + Value::Int(key) + ); if !object_value { - assert_eq!(slots.peek(&window, 0).unwrap(), &Value::Int(17)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 0).unwrap()), + Value::Int(17) + ); } if let Some(Value::Object(view)) = &retained { if source.starts_with("new Uint8Array") && !detached { @@ -1873,7 +2194,7 @@ mod tests { ); } } - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } } @@ -1887,10 +2208,12 @@ mod tests { owner.metadata.max_stack = 3; let mut slots = SlotStore::new(3); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for value in [base, Value::Int(0), Value::Int(17)] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } { let _borrow = runtime.0.state.borrow(); @@ -1916,7 +2239,10 @@ mod tests { .unwrap() ); assert_eq!(window.depth, 3); - assert_eq!(slots.peek(&window, 0).unwrap(), &Value::Int(17)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 0).unwrap()), + Value::Int(17) + ); assert!(runtime.0.deferred_references.has_pending()); runtime.drain_deferred_references().unwrap(); let Value::Object(view) = &retained else { @@ -1938,7 +2264,7 @@ mod tests { runtime.typed_array_read_index(view, 0).unwrap(), Some(Value::Int(17)) ); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -1961,10 +2287,14 @@ mod tests { code.metadata.max_stack = 2; let mut store = SlotStore::new(2); let mut window = store - .push_frame(&code.frame_layout(), empty_storage()) + .push_frame(&runtime, &code.frame_layout(), empty_storage()) + .unwrap(); + store + .push(&mut window, into_internal(&runtime, base)) + .unwrap(); + store + .push(&mut window, into_internal(&runtime, Value::String(key))) .unwrap(); - store.push(&mut window, base).unwrap(); - store.push(&mut window, Value::String(key)).unwrap(); assert_eq!( store .run_window(&mut window) @@ -1975,13 +2305,19 @@ mod tests { "{text}/{retained}" ); if expected { - assert_eq!(store.peek(&window, 0).unwrap(), &Value::Int(42)); + assert_eq!( + to_public(&runtime, store.peek(&window, 0).unwrap()), + Value::Int(42) + ); assert_eq!(window.depth, 1); } else { assert_eq!(window.depth, 2); - assert!(matches!(store.peek(&window, 0).unwrap(), Value::String(_))); + assert!(matches!( + store.peek(&window, 0).unwrap(), + JsValue::String(_) + )); } - store.clear_frame(window).unwrap(); + store.clear_frame(&runtime, window).unwrap(); drop(keep_key); drop(keep_base); } @@ -2017,10 +2353,12 @@ mod tests { owner.metadata.max_stack = 3; let mut slots = SlotStore::new(3); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for value in [Value::Int(99), base, key.clone()] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } assert!( !slots @@ -2031,12 +2369,15 @@ mod tests { "{source}" ); assert_eq!(window.depth, 3); - assert_eq!(slots.peek(&window, 0).unwrap(), &key); + assert_eq!(to_public(&runtime, slots.peek(&window, 0).unwrap()), key); assert!( - matches!(slots.peek(&window, 1).unwrap(), Value::Object(root) if root.object_id()==id) + matches!(slots.peek(&window, 1).unwrap(), JsValue::Object(handle) if *handle == id) ); - assert_eq!(slots.peek(&window, 2).unwrap(), &Value::Int(99)); - slots.clear_frame(window).unwrap(); + assert_eq!( + to_public(&runtime, slots.peek(&window, 2).unwrap()), + Value::Int(99) + ); + slots.clear_frame(&runtime, window).unwrap(); } let runtime = Runtime::new(); let mut context = runtime.new_context(); @@ -2046,10 +2387,12 @@ mod tests { owner.metadata.max_stack = 3; let mut slots = SlotStore::new(3); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for value in [Value::Int(99), base, Value::Int(0)] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } let queued = runtime.new_object(None).unwrap(); { @@ -2081,10 +2424,16 @@ mod tests { .unwrap() ); assert_eq!(window.depth, 2); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(42)); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(99)); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(42) + ); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(99) + ); drop(retained); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -2115,10 +2464,12 @@ mod tests { owner.metadata.max_stack = 2; let mut slots = SlotStore::new(2); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for value in [base, Value::Int(0)] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } assert!( !slots @@ -2129,11 +2480,14 @@ mod tests { "{source}" ); assert_eq!(window.depth, 2); - assert_eq!(slots.peek(&window, 0).unwrap(), &Value::Int(0)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 0).unwrap()), + Value::Int(0) + ); assert!( - matches!(slots.peek(&window, 1).unwrap(), Value::Object(root) if root.object_id()==id) + matches!(slots.peek(&window, 1).unwrap(), JsValue::Object(handle) if *handle == id) ); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } let runtime = Runtime::new(); let mut context = runtime.new_context(); @@ -2143,10 +2497,12 @@ mod tests { owner.metadata.max_stack = 2; let mut slots = SlotStore::new(2); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for value in [base, Value::Int(0)] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } let queued = runtime.new_object(None).unwrap(); { @@ -2178,8 +2534,11 @@ mod tests { .unwrap() ); assert_eq!(window.depth, 1); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(42)); - slots.clear_frame(window).unwrap(); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(42) + ); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -2204,23 +2563,26 @@ mod tests { ); let mut slots = SlotStore::new(16); let mut parent = slots - .push_frame(&caller.frame_layout(), empty_storage()) + .push_frame(&runtime, &caller.frame_layout(), empty_storage()) + .unwrap(); + slots + .push(&mut parent, into_internal(&runtime, Value::Int(99))) .unwrap(); - slots.push(&mut parent, Value::Int(99)).unwrap(); let mut child = slots .push_frame( + &runtime, &callee.frame_layout(), FrameStorage { - original_arguments: vec![Value::Int(10)], + original_arguments: vec![JsValue::Int(10)], parameters: vec![ - FrameBinding::Direct(Value::Int(10)), - FrameBinding::Direct(Value::Undefined), + FrameBinding::Direct(JsValue::Int(10)), + FrameBinding::Direct(JsValue::Undefined), ], locals: vec![ - FrameBinding::Direct(Value::Int(20)), + FrameBinding::Direct(JsValue::Int(20)), FrameBinding::Uninitialized, ], - operands: vec![Value::Int(30)], + operands: vec![JsValue::Int(30)], }, ) .unwrap(); @@ -2232,24 +2594,36 @@ mod tests { assert!(slots.peek(&parent, 0).is_err()); assert!(slots.parameter(&child, 2).is_err()); assert!(slots.local(&child, 2).is_err()); - slots.push(&mut child, Value::Int(31)).unwrap(); - assert!(slots.push(&mut child, Value::Int(32)).is_err()); + slots + .push(&mut child, into_internal(&runtime, Value::Int(31))) + .unwrap(); + assert!( + slots + .push(&mut child, into_internal(&runtime, Value::Int(32))) + .is_err() + ); let end = child.end; child.end = end - 1; assert!(slots.pop(&mut child).is_err()); child.end = end; assert_eq!(slots.depth(&child), 2); - assert_eq!(slots.pop(&mut child).unwrap(), Value::Int(31)); - let storage = slots.take_frame(child).unwrap(); - assert_eq!(storage.original_arguments, vec![Value::Int(10)]); - assert_eq!(storage.operands, vec![Value::Int(30)]); + assert_eq!( + take_public(&runtime, slots.pop(&mut child).unwrap()), + Value::Int(31) + ); + let storage = slots.take_frame(&runtime, child).unwrap(); + assert_eq!(storage.original_arguments, vec![JsValue::Int(10)]); + assert_eq!(storage.operands, vec![JsValue::Int(30)]); assert!(matches!( storage.locals[0], - FrameBinding::Direct(Value::Int(20)) + FrameBinding::Direct(JsValue::Int(20)) )); assert!(matches!(storage.locals[1], FrameBinding::Uninitialized)); - assert_eq!(slots.pop(&mut parent).unwrap(), Value::Int(99)); - slots.clear_frame(parent).unwrap(); + assert_eq!( + take_public(&runtime, slots.pop(&mut parent).unwrap()), + Value::Int(99) + ); + slots.clear_frame(&runtime, parent).unwrap(); assert_eq!(slots.active_end, 0); assert!(slots.slots.iter().all(Option::is_none)); #[cfg(target_pointer_width = "64")] @@ -2264,10 +2638,10 @@ mod tests { let owner = PublishedFunctionSnapshot::empty_for_test(context.realm); let mut slots = SlotStore::new(0); let parent = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let child = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); for window in [&parent, &child] { assert_eq!(window.whole(), 0..0); @@ -2279,9 +2653,9 @@ mod tests { assert_ne!(parent.id, child.id); assert!(slots.binding_counts(&parent).is_err()); assert_eq!(slots.binding_counts(&child).unwrap(), (0, 0)); - slots.clear_frame(child).unwrap(); + slots.clear_frame(&runtime, child).unwrap(); assert_eq!(slots.binding_counts(&parent).unwrap(), (0, 0)); - slots.clear_frame(parent).unwrap(); + slots.clear_frame(&runtime, parent).unwrap(); assert!(slots.windows.is_empty()); } @@ -2297,7 +2671,7 @@ mod tests { let marker = runtime.new_object(None).unwrap(); let mut slots = SlotStore::new(32); let mut parent = slots - .push_frame(&caller.frame_layout(), empty_storage()) + .push_frame(&runtime, &caller.frame_layout(), empty_storage()) .unwrap(); for value in [ Value::Int(99), @@ -2306,13 +2680,19 @@ mod tests { Value::Int(7), Value::Object(marker.clone()), ] { - slots.push(&mut parent, value).unwrap(); + slots + .push(&mut parent, into_internal(&runtime, value)) + .unwrap(); } { - let _borrow = runtime.0.state.borrow(); + // Retaining a root while the state is mutably borrowed is the + // injected failure: the shared-borrow fast path added for nested + // materialization only applies to shared borrows. + let _borrow = runtime.0.state.borrow_mut(); assert!( slots .push_call_frame( + &runtime, &callee.frame_layout(), &mut parent, 2, @@ -2323,13 +2703,17 @@ mod tests { .is_err() ); assert_eq!(slots.depth(&parent), 5); - assert_eq!(slots.peek(&parent, 1).unwrap(), &Value::Int(7)); + assert_eq!( + to_public(&runtime, slots.peek(&parent, 1).unwrap()), + Value::Int(7) + ); assert_eq!(slots.active_end, parent.whole().end); assert!(slots.slots[slots.active_end..].iter().all(Option::is_none)); assert_eq!(slots.windows.len(), 1); } let child = slots .push_call_frame( + &runtime, &callee.frame_layout(), &mut parent, 2, @@ -2340,21 +2724,27 @@ mod tests { .unwrap(); assert_eq!(slots.depth(&parent), 1); assert!(slots.peek(&parent, 0).is_err()); - let storage = slots.take_frame(child).unwrap(); + let storage = slots.take_frame(&runtime, child).unwrap(); assert_eq!(storage.original_arguments.len(), 2); - assert_eq!(storage.original_arguments[0], Value::Int(7)); + assert_eq!( + to_public(&runtime, &storage.original_arguments[0]), + Value::Int(7) + ); assert_eq!(storage.parameters.len(), 3); assert!(matches!( storage.parameters[2], - FrameBinding::Direct(Value::Undefined) + FrameBinding::Direct(JsValue::Undefined) )); - assert_eq!(slots.peek(&parent, 0).unwrap(), &Value::Int(99)); + assert_eq!( + to_public(&runtime, slots.peek(&parent, 0).unwrap()), + Value::Int(99) + ); assert!( slots.slots[parent.operands().start + 1..parent.operands().end] .iter() .all(Option::is_none) ); - slots.clear_frame(parent).unwrap(); + slots.clear_frame(&runtime, parent).unwrap(); } #[test] @@ -2367,17 +2757,29 @@ mod tests { let object = runtime.new_object(None).unwrap(); let mut slots = SlotStore::new(16); let source = || FrameStorage { - original_arguments: vec![Value::Int(7), Value::Object(object.clone())], + original_arguments: vec![ + JsValue::Int(7), + JsValue::Object(object.clone().into_handle()), + ], parameters: Vec::new(), locals: Vec::new(), operands: Vec::new(), }; let storage = source(); { - let _borrow = runtime.0.state.borrow(); + // Retaining a root while the state is mutably borrowed is the + // injected failure: the shared-borrow fast path added for nested + // materialization only applies to shared borrows. + let _borrow = runtime.0.state.borrow_mut(); assert!( slots - .push_initialized_frame(&owner.frame_layout(), storage, &function, None) + .push_initialized_frame( + &runtime, + &owner.frame_layout(), + storage, + &function, + None + ) .is_err() ); assert_eq!(slots.active_end, 0); @@ -2385,22 +2787,25 @@ mod tests { assert!(slots.windows.is_empty()); } let window = slots - .push_initialized_frame(&owner.frame_layout(), source(), &function, None) + .push_initialized_frame(&runtime, &owner.frame_layout(), source(), &function, None) .unwrap(); assert_eq!(slots.binding_counts(&window).unwrap(), (0, 3)); assert!(matches!( slots.parameter(&window, 2).unwrap(), - FrameBinding::Direct(Value::Undefined) + FrameBinding::Direct(JsValue::Undefined) )); slots - .replace_parameter(&window, 0, FrameBinding::Direct(Value::Int(9))) + .replace_parameter(&window, 0, FrameBinding::Direct(JsValue::Int(9))) .unwrap(); - let storage = slots.take_frame(window).unwrap(); + let storage = slots.take_frame(&runtime, window).unwrap(); assert_eq!(storage.original_arguments.len(), 2); - assert_eq!(storage.original_arguments[0], Value::Int(7)); + assert_eq!( + to_public(&runtime, &storage.original_arguments[0]), + Value::Int(7) + ); assert!(matches!( storage.parameters[0], - FrameBinding::Direct(Value::Int(9)) + FrameBinding::Direct(JsValue::Int(9)) )); } @@ -2420,22 +2825,25 @@ mod tests { let mut owner = PublishedFunctionSnapshot::empty_for_test(context.realm); owner.metadata.max_stack = size; let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); slots - .push(&mut window, Value::Object(marker.clone())) + .push( + &mut window, + into_internal(&runtime, Value::Object(marker.clone())), + ) .unwrap(); windows.push(window); } assert_eq!(slots.active_end, 12); assert_eq!(slots.slots.len(), 12); while let Some(window) = windows.pop() { - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); assert!(slots.slots[slots.active_end..].iter().all(Option::is_none)); if let Some(parent) = windows.last() { assert_eq!( - slots.peek(parent, 0).unwrap(), - &Value::Object(marker.clone()) + to_public(&runtime, slots.peek(parent, 0).unwrap()), + Value::Object(marker.clone()) ); } } @@ -2464,25 +2872,39 @@ mod tests { #[test] fn initialized_suffix_rolls_back_after_a_successful_object_copy() { let runtime = Runtime::new(); - let other_runtime = Runtime::new(); let context = runtime.new_context(); let mut owner = PublishedFunctionSnapshot::empty_for_test(context.realm); owner.metadata.argument_count = 3; let function = runtime.new_object(None).unwrap(); let first = runtime.new_object(None).unwrap(); let first_id = first.object_id(); - let blocked = other_runtime.new_object(None).unwrap(); - let storage = FrameStorage { - original_arguments: vec![Value::Object(first.clone()), Value::Object(blocked.clone())], - ..empty_storage() - }; + let blocked = runtime.new_object(None).unwrap(); + let blocked_handle = blocked.into_handle(); let mut slots = SlotStore::new(16); + // Drop the second argument's only root and collect it so its handle is + // stale. The first retain then succeeds while the second fails, which + // exercises suffix rollback without relying on cross-runtime brands. + runtime + .release_jsvalue(JsValue::Object(blocked_handle)) + .unwrap(); + runtime.run_gc().unwrap(); { - // First retain succeeds in its runtime; the second retain fails. - let _borrow = other_runtime.0.state.borrow(); + let storage = FrameStorage { + original_arguments: vec![ + JsValue::Object(first.clone().into_handle()), + JsValue::Object(blocked_handle), + ], + ..empty_storage() + }; assert!( slots - .push_initialized_frame(&owner.frame_layout(), storage, &function, None) + .push_initialized_frame( + &runtime, + &owner.frame_layout(), + storage, + &function, + None + ) .is_err() ); assert_eq!(slots.active_end, 0); @@ -2493,11 +2915,16 @@ mod tests { runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(first_id).is_err()); let window = slots - .push_initialized_frame(&owner.frame_layout(), empty_storage(), &function, None) + .push_initialized_frame( + &runtime, + &owner.frame_layout(), + empty_storage(), + &function, + None, + ) .unwrap(); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); assert!(slots.slots.iter().all(Option::is_none)); - other_runtime.run_gc().unwrap(); } #[test] @@ -2512,30 +2939,33 @@ mod tests { let mut slots = SlotStore::new(16); let window = slots .push_frame( + &runtime, &owner.frame_layout(), FrameStorage { - original_arguments: vec![Value::Int(1), Value::Int(2)], + original_arguments: vec![JsValue::Int(1), JsValue::Int(2)], parameters: vec![ - FrameBinding::Direct(Value::Int(3)), - FrameBinding::Direct(Value::Int(4)), + FrameBinding::Direct(JsValue::Int(3)), + FrameBinding::Direct(JsValue::Int(4)), ], locals: Vec::new(), - operands: vec![Value::Object(value)], + operands: vec![JsValue::Object(value.into_handle())], }, ) .unwrap(); - let storage = slots.take_frame(window).unwrap(); + let storage = slots.take_frame(&runtime, window).unwrap(); let initialized = slots.slots.len(); assert_eq!(slots.active_end, 0); assert!(slots.slots.iter().all(Option::is_none)); runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(value_id).is_ok()); - let window = slots.push_frame(&owner.frame_layout(), storage).unwrap(); + let window = slots + .push_frame(&runtime, &owner.frame_layout(), storage) + .unwrap(); assert_eq!(slots.slots.len(), initialized); assert!( - matches!(slots.peek(&window, 0).unwrap(), Value::Object(value) if value.object_id() == value_id) + matches!(slots.peek(&window, 0).unwrap(), JsValue::Object(handle) if *handle == value_id) ); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(value_id).is_err()); assert!(slots.slots.iter().all(Option::is_none)); @@ -2573,39 +3003,49 @@ mod tests { owner.metadata.max_stack = 2; let mut slots = SlotStore::new(8); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); assert!( slots .binary_number(&mut window, |_, _| unreachable!()) .is_err() ); - slots.push(&mut window, Value::Int(7)).unwrap(); + slots + .push(&mut window, into_internal(&runtime, Value::Int(7))) + .unwrap(); let object = runtime.new_object(None).unwrap(); let id = object.object_id(); - slots.push(&mut window, Value::Object(object)).unwrap(); + slots + .push(&mut window, into_internal(&runtime, Value::Object(object))) + .unwrap(); assert!( !slots .binary_number(&mut window, |_, _| unreachable!()) .unwrap() ); assert_eq!(slots.depth(&window), 2); - assert_eq!(slots.peek(&window, 1).unwrap(), &Value::Int(7)); + assert_eq!( + to_public(&runtime, slots.peek(&window, 1).unwrap()), + Value::Int(7) + ); assert!( - matches!(slots.peek(&window, 0).unwrap(), Value::Object(root) if root.object_id() == id) + matches!(slots.peek(&window, 0).unwrap(), JsValue::Object(handle) if *handle == id) ); - drop(slots.pop(&mut window).unwrap()); + let dead_owner = slots.pop(&mut window).unwrap(); + runtime.release_jsvalue(dead_owner).unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_err()); - slots.push(&mut window, Value::Int(3)).unwrap(); + slots + .push(&mut window, into_internal(&runtime, Value::Int(3))) + .unwrap(); let child = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); assert!( slots .binary_number(&mut window, |_, _| unreachable!()) .is_err() ); - slots.clear_frame(child).unwrap(); + slots.clear_frame(&runtime, child).unwrap(); assert!( slots .binary_number(&mut window, |left, right| left.sub(right).into()) @@ -2613,8 +3053,11 @@ mod tests { ); assert_eq!(slots.depth(&window), 1); assert!(slots.slots[window.operands().start + 1].is_none()); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(4)); - slots.clear_frame(window).unwrap(); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(4) + ); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -2625,27 +3068,29 @@ mod tests { owner.metadata.max_stack = 1; let mut slots = SlotStore::new(8192); let mut parent = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let object = runtime.new_object(None).unwrap(); let id = object.object_id(); - slots.push(&mut parent, Value::Object(object)).unwrap(); + slots + .push(&mut parent, into_internal(&runtime, Value::Object(object))) + .unwrap(); let original_capacity = slots.slots.capacity(); owner.metadata.max_stack = 4096; let child = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); assert!(slots.slots.capacity() > original_capacity); assert!( slots.peek(&parent, 0).is_err(), "an inactive parent cannot access the current window" ); - slots.clear_frame(child).unwrap(); + slots.clear_frame(&runtime, child).unwrap(); let result = slots.pop(&mut parent).unwrap(); assert!(slots.slots[parent.operands().start].is_none()); - slots.clear_frame(parent).unwrap(); + slots.clear_frame(&runtime, parent).unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_ok()); - drop(result); + runtime.release_jsvalue(result).unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_err()); assert_eq!(slots.active_end, 0); assert!(slots.slots.iter().all(Option::is_none)); @@ -2660,7 +3105,7 @@ mod tests { owner.metadata.max_stack = 6; let mut slots = SlotStore::new(6); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let object = runtime.new_object(None).unwrap(); let id = object.object_id(); @@ -2671,14 +3116,16 @@ mod tests { Value::Object(object), Value::Int(4), ] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } let capacity = slots.slots.capacity(); slots.rotate_operands(&window, 1, 4, false).unwrap(); slots.rotate_operands(&window, 0, 4, true).unwrap(); - slots.insert_copy(&mut window, 4, 5).unwrap(); + slots.insert_copy(&runtime, &mut window, 4, 5).unwrap(); assert_eq!(slots.slots.capacity(), capacity); - assert!(slots.insert_copy(&mut window, 0, 0).is_err()); + assert!(slots.insert_copy(&runtime, &mut window, 0, 0).is_err()); assert!(slots.rotate_operands(&window, 1, 6, false).is_err()); assert!( slots @@ -2686,18 +3133,25 @@ mod tests { .is_err() ); assert_eq!(slots.depth(&window), 6); - let mut values = slots.take_frame(window).unwrap().operands; + let mut values = slots.take_frame(&runtime, window).unwrap().operands; for expected in [0, 4, 2, 1] { - assert_eq!(values.pop().unwrap(), Value::Int(expected)); + assert_eq!( + take_public(&runtime, values.pop().unwrap()), + Value::Int(expected) + ); } assert!( values .iter() - .all(|value| matches!(value, Value::Object(root) if root.object_id() == id)) + .all(|value| matches!(value, JsValue::Object(handle) if *handle == id)) ); - drop(values.pop()); + if let Some(value) = values.pop() { + runtime.release_jsvalue(value).unwrap(); + } assert!(runtime.0.state.borrow().heap.object(id).is_ok()); - drop(values); + for value in values { + runtime.release_jsvalue(value).unwrap(); + } assert!(runtime.0.state.borrow().heap.object(id).is_err()); assert_eq!(slots.active_end, 0); assert!(slots.slots.iter().all(Option::is_none)); @@ -2711,25 +3165,33 @@ mod tests { owner.metadata.max_stack = 6; let mut slots = SlotStore::new(6); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let object = runtime.new_object(None).unwrap(); let id = object.object_id(); for value in [Value::Int(7), Value::Object(object), Value::Int(9)] { - slots.push(&mut window, value).unwrap(); + slots + .push(&mut window, into_internal(&runtime, value)) + .unwrap(); } - slots.duplicate_operands(&mut window, 3).unwrap(); - assert!(slots.duplicate_operands(&mut window, 3).is_err()); + slots.duplicate_operands(&runtime, &mut window, 3).unwrap(); + assert!(slots.duplicate_operands(&runtime, &mut window, 3).is_err()); assert_eq!(slots.depth(&window), 6); for _ in 0..2 { - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(9)); - assert!( - matches!(slots.pop(&mut window).unwrap(), Value::Object(root) if root.object_id() == id) + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(9) + ); + let popped = slots.pop(&mut window).unwrap(); + assert!(matches!(&popped, JsValue::Object(handle) if *handle == id)); + runtime.release_jsvalue(popped).unwrap(); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(7) ); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(7)); } assert!(runtime.0.state.borrow().heap.object(id).is_err()); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -2740,29 +3202,38 @@ mod tests { owner.metadata.max_stack = 6; let mut slots = SlotStore::new(6); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let object = runtime.new_object(None).unwrap(); - let id = object.object_id(); + let stale_handle = object.into_handle(); + // Drop the object's only root and collect it so its handle is stale; + // the String copy then commits while the Object retain fails. + runtime + .release_jsvalue(JsValue::Object(stale_handle)) + .unwrap(); + runtime.run_gc().unwrap(); let text = Value::String(crate::engine::value::JsString::from_static( "retained prefix", )); - for value in [text.clone(), Value::Object(object), Value::Int(9)] { + for value in [ + into_internal(&runtime, text.clone()), + JsValue::Object(stale_handle), + into_internal(&runtime, Value::Int(9)), + ] { slots.push(&mut window, value).unwrap(); } - { - // Rc-backed String copies succeed; the following Object retain - // must fail because it needs a mutable heap borrow. - let state = runtime.0.state.borrow(); - assert!(slots.duplicate_operands(&mut window, 3).is_err()); - assert_eq!(slots.depth(&window), 4); - assert_eq!(slots.peek(&window, 0).unwrap(), &text); - assert_eq!(slots.peek(&window, 1).unwrap(), &Value::Int(9)); - assert!(state.heap.object(id).is_ok()); - assert!(!runtime.0.deferred_references.has_pending()); - } - slots.clear_frame(window).unwrap(); - assert!(runtime.0.state.borrow().heap.object(id).is_err()); + assert!(slots.duplicate_operands(&runtime, &mut window, 3).is_err()); + assert_eq!(slots.depth(&window), 4); + assert_eq!(to_public(&runtime, slots.peek(&window, 0).unwrap()), text); + assert_eq!( + to_public(&runtime, slots.peek(&window, 1).unwrap()), + Value::Int(9) + ); + assert!(!runtime.0.deferred_references.has_pending()); + // The intentionally stale handle owns no edge: it was never retained by + // the failed transaction, so it must not reach frame teardown. + slots.slots[window.operands().start + 1].take(); + slots.clear_frame(&runtime, window).unwrap(); } #[test] @@ -2777,11 +3248,13 @@ mod tests { let mut previous = None; for _ in 0..2 { let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) + .unwrap(); + slots + .push(&mut window, into_internal(&runtime, Value::Int(1))) .unwrap(); - slots.push(&mut window, Value::Int(1)).unwrap(); - slots.insert_copy(&mut window, 0, 0).unwrap(); - slots.clear_frame(window).unwrap(); + slots.insert_copy(&runtime, &mut window, 0, 0).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); let cost = profile.snapshot().owned_storage; assert_eq!(cost.slot_capacity_growths, 1); assert_eq!(cost.maximum_reserved_slots, 2); @@ -2815,13 +3288,13 @@ mod tests { owner.metadata.max_stack = 5; let mut slots = SlotStore::new(8); let warm = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); - slots.clear_frame(warm).unwrap(); + slots.clear_frame(&runtime, warm).unwrap(); owner.metadata.max_stack = 2; let profile = crate::engine::api::profiling::CostProfile::start(); let reused = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let costs = profile.snapshot().owned_storage; assert_eq!(costs.physical_none_initializations, 0); @@ -2830,7 +3303,7 @@ mod tests { assert_eq!(costs.slots_initialized, 2); assert_eq!(costs.slot_capacity_growths, 0); assert!(costs.maximum_slot_capacity >= 5); - slots.clear_frame(reused).unwrap(); + slots.clear_frame(&runtime, reused).unwrap(); } #[test] @@ -2842,30 +3315,31 @@ mod tests { let mut slots = SlotStore::new(32); let window = slots .push_frame( + &runtime, &owner.frame_layout(), FrameStorage { - original_arguments: vec![Value::Int(1)], + original_arguments: vec![JsValue::Int(1)], parameters: vec![ - FrameBinding::Direct(Value::Int(1)), - FrameBinding::Direct(Value::Undefined), + FrameBinding::Direct(JsValue::Int(1)), + FrameBinding::Direct(JsValue::Undefined), ], ..empty_storage() }, ) .unwrap(); let old = slots - .replace_parameter(&window, 0, FrameBinding::Direct(Value::Int(2))) + .replace_parameter(&window, 0, FrameBinding::Direct(JsValue::Int(2))) .unwrap(); - assert!(matches!(old, FrameBinding::Direct(Value::Int(1)))); - let storage = slots.take_frame(window).unwrap(); - assert_eq!(storage.original_arguments, vec![Value::Int(1)]); + assert!(matches!(old, FrameBinding::Direct(JsValue::Int(1)))); + let storage = slots.take_frame(&runtime, window).unwrap(); + assert_eq!(storage.original_arguments, vec![JsValue::Int(1)]); assert!(matches!( storage.parameters[0], - FrameBinding::Direct(Value::Int(2)) + FrameBinding::Direct(JsValue::Int(2)) )); assert!(matches!( storage.parameters[1], - FrameBinding::Direct(Value::Undefined) + FrameBinding::Direct(JsValue::Undefined) )); } @@ -2878,17 +3352,27 @@ mod tests { let mut first = SlotStore::new(4); let mut second = SlotStore::new(4); let mut a = first - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); let mut b = second - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) + .unwrap(); + first + .push(&mut a, into_internal(&runtime, Value::Int(1))) + .unwrap(); + second + .push(&mut b, into_internal(&runtime, Value::Int(2))) .unwrap(); - first.push(&mut a, Value::Int(1)).unwrap(); - second.push(&mut b, Value::Int(2)).unwrap(); assert!(second.peek(&a, 0).is_err()); assert!(first.peek(&b, 0).is_err()); - assert_eq!(first.pop(&mut a).unwrap(), Value::Int(1)); - assert_eq!(second.pop(&mut b).unwrap(), Value::Int(2)); + assert_eq!( + take_public(&runtime, first.pop(&mut a).unwrap()), + Value::Int(1) + ); + assert_eq!( + take_public(&runtime, second.pop(&mut b).unwrap()), + Value::Int(2) + ); } #[test] @@ -2899,18 +3383,25 @@ mod tests { owner.metadata.max_stack = 1; let mut slots = SlotStore::new(1); let mut window = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); - slots.push(&mut window, Value::Int(7)).unwrap(); - assert!(slots.push(&mut window, Value::Int(8)).is_err()); + slots + .push(&mut window, into_internal(&runtime, Value::Int(7))) + .unwrap(); + assert!( + slots + .push(&mut window, into_internal(&runtime, Value::Int(8))) + .is_err() + ); assert!( slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .is_err() ); assert!( slots .push_frame( + &runtime, &owner.frame_layout(), FrameStorage { locals: vec![FrameBinding::Uninitialized], @@ -2921,11 +3412,14 @@ mod tests { ); assert!(slots.peek(&window, usize::MAX).is_err()); assert_eq!(slots.depth(&window), 1); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(7)); + assert_eq!( + take_public(&runtime, slots.pop(&mut window).unwrap()), + Value::Int(7) + ); assert!(slots.pop(&mut window).is_err()); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); let reused = slots - .push_frame(&owner.frame_layout(), empty_storage()) + .push_frame(&runtime, &owner.frame_layout(), empty_storage()) .unwrap(); assert_eq!(slots.depth(&reused), 0); assert!(slots.slots.iter().all(Option::is_none)); diff --git a/src/engine/vm/stack/call.rs b/src/engine/vm/stack/call.rs index a38ee089..bbe2908a 100644 --- a/src/engine/vm/stack/call.rs +++ b/src/engine/vm/stack/call.rs @@ -4,6 +4,7 @@ impl SlotStore { #[allow(clippy::too_many_arguments)] pub(in crate::engine::vm) fn push_ordinary_frame( &mut self, + runtime: &Runtime, layout: &FrameLayout<'_>, parent: &mut FrameWindow, count: usize, @@ -26,11 +27,11 @@ impl SlotStore { if index >= start && !matches!( value, - Value::Undefined - | Value::Null - | Value::Bool(_) - | Value::Int(_) - | Value::Float(_) + JsValue::Undefined + | JsValue::Null + | JsValue::Bool(_) + | JsValue::Int(_) + | JsValue::Float(_) ) { // Preserve every original non-scalar owner until frame teardown, @@ -82,35 +83,40 @@ impl SlotStore { }; #[cfg(feature = "profiling")] { - roots += usize::from(matches!(value, Value::Object(_) | Value::Symbol(_))); + roots += usize::from(matches!(value, JsValue::Object(_) | JsValue::Symbol(_))); } - match copy_value(value) { + match copy_value(runtime, value) { Ok(value) => { self.slots[original_end + index] = Some(FrameBinding::Direct(value)) } Err(error) => { - self.clear_unpublished(original_end..original_end + index); + let _ = self.clear_unpublished(runtime, original_end..original_end + index); return Err(error); } } } } for index in original_end + count..parameters_end { - self.slots[index] = Some(FrameBinding::Direct(Value::Undefined)); + self.slots[index] = Some(FrameBinding::Direct(JsValue::Undefined)); } for (index, definition) in layout.locals().iter().enumerate() { - self.slots[parameters_end + index] = - Some(super::super::call::prepare::initial_local_binding( + self.slots[parameters_end + index] = Some( + super::super::call::prepare::initial_local_binding( + runtime, definition.is_lexical, function_name == Some(index as u16), function, - )); + ) + .map_err(runtime_error_to_vm_error)?, + ); } for index in 0..count { self.slots[base + index] = self.slots[start + index].take(); } for index in start - 1 - usize::from(method)..start { - self.slots[index].take(); + if let Some(binding) = self.slots[index].take() { + release_binding(runtime, binding)?; + } } parent.depth -= consumed; self.active_end = end; @@ -172,7 +178,7 @@ impl SlotStore { mod tests { use super::*; use crate::engine::code::runtime::PublishedFunctionSnapshot; - fn storage(values: Vec) -> FrameStorage { + fn storage(values: Vec) -> FrameStorage { FrameStorage { original_arguments: vec![], parameters: vec![], @@ -190,12 +196,17 @@ mod tests { let mut slots = SlotStore::new(32); let mut parent = slots .push_frame( + &runtime, &executable.frame_layout(), - storage(vec![Value::Object(function.clone()), Value::Int(7)]), + storage(vec![ + JsValue::Object(function.clone().into_handle()), + JsValue::Int(7), + ]), ) .unwrap(); let child = slots .push_ordinary_frame( + &runtime, &executable.frame_layout(), &mut parent, 1, @@ -209,20 +220,26 @@ mod tests { assert_eq!(slots.actual_argument_count(&child).unwrap(), 1); assert!(matches!( slots.parameter(&child, 0).unwrap(), - FrameBinding::Direct(Value::Int(7)) + FrameBinding::Direct(JsValue::Int(7)) )); assert_eq!( - slots.take_frame(child).unwrap().original_arguments, - vec![Value::Undefined] + slots + .take_frame(&runtime, child) + .unwrap() + .original_arguments, + vec![JsValue::Undefined] ); let marker = runtime.new_object(None).unwrap(); let marker_id = marker.object_id(); slots - .push(&mut parent, Value::Object(function.clone())) + .push(&mut parent, JsValue::Object(function.clone().into_handle())) + .unwrap(); + slots + .push(&mut parent, JsValue::Object(marker.into_handle())) .unwrap(); - slots.push(&mut parent, Value::Object(marker)).unwrap(); let child = slots .push_ordinary_frame( + &runtime, &executable.frame_layout(), &mut parent, 1, @@ -233,41 +250,49 @@ mod tests { ) .unwrap(); assert_eq!(child.original_arguments().len(), 1); - slots - .replace_parameter(&child, 0, FrameBinding::Direct(Value::Undefined)) + let replaced = slots + .replace_parameter(&child, 0, FrameBinding::Direct(JsValue::Undefined)) .unwrap(); + release_binding(&runtime, replaced).unwrap(); runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(marker_id).is_ok()); - slots.clear_frame(child).unwrap(); + slots.clear_frame(&runtime, child).unwrap(); assert!(runtime.0.state.borrow().heap.object(marker_id).is_err()); - slots.clear_frame(parent).unwrap(); + slots.clear_frame(&runtime, parent).unwrap(); } #[test] fn ordinary_retain_failure_keeps_the_entire_parent_and_rolls_back_suffix() { let runtime = Runtime::new(); - let other = Runtime::new(); let context = runtime.new_context(); let function = runtime.new_object(None).unwrap(); let first = runtime.new_object(None).unwrap(); - let blocked = other.new_object(None).unwrap(); + let blocked = runtime.new_object(None).unwrap(); + let stale_handle = blocked.into_handle(); + // Make the top argument's handle stale so its retain fails after the + // earlier operands commit, exercising suffix rollback. + runtime + .release_jsvalue(JsValue::Object(stale_handle)) + .unwrap(); + runtime.run_gc().unwrap(); let mut executable = PublishedFunctionSnapshot::empty_for_test(context.realm); executable.metadata.max_stack = 4; let mut slots = SlotStore::new(32); let mut parent = slots .push_frame( + &runtime, &executable.frame_layout(), storage(vec![ - Value::Object(function.clone()), - Value::Object(first), - Value::Object(blocked), + JsValue::Object(function.clone().into_handle()), + JsValue::Object(first.into_handle()), + JsValue::Object(stale_handle), ]), ) .unwrap(); let end = slots.active_end; - let borrow = other.0.state.borrow(); assert!( slots .push_ordinary_frame( + &runtime, &executable.frame_layout(), &mut parent, 2, @@ -281,7 +306,12 @@ mod tests { assert_eq!(slots.active_end, end); assert_eq!(slots.depth(&parent), 3); assert!(slots.slots[end..].iter().all(Option::is_none)); - drop(borrow); - slots.clear_frame(parent).unwrap(); + // Swap the reclaimed top operand for a live binding before clearing so + // frame teardown never releases a stale handle. + let stale = slots + .replace_operand(&parent, 0, JsValue::Undefined) + .unwrap(); + drop(stale); + slots.clear_frame(&runtime, parent).unwrap(); } } diff --git a/src/engine/vm/stack/number.rs b/src/engine/vm/stack/number.rs index de727fcb..150ee480 100644 --- a/src/engine/vm/stack/number.rs +++ b/src/engine/vm/stack/number.rs @@ -101,7 +101,7 @@ mod tests { use crate::engine::api::Runtime; use crate::engine::code::function::metadata::{ClosureVariableKind, VariableDefinition}; use crate::engine::code::runtime::PublishedFunctionSnapshot; - use crate::engine::value::Value; + use crate::engine::value::JsValue; use crate::engine::vm::stack::FrameStorage; use std::rc::Rc; @@ -120,11 +120,12 @@ mod tests { let mut slots = SlotStore::new(20); let window = slots .push_frame( + &runtime, &owner.frame_layout(), FrameStorage { original_arguments: Vec::new(), parameters: Vec::new(), - locals: vec![FrameBinding::Direct(Value::Int(7))], + locals: vec![FrameBinding::Direct(JsValue::Int(7))], operands: Vec::new(), }, ) @@ -138,8 +139,10 @@ mod tests { let (mut slots, mut window) = frame(&runtime, 2); let object = runtime.new_object(None).unwrap(); let id = object.object_id(); - slots.push(&mut window, Value::Int(3)).unwrap(); - slots.push(&mut window, Value::Object(object)).unwrap(); + slots.push(&mut window, JsValue::Int(3)).unwrap(); + slots + .push(&mut window, JsValue::Object(object.into_handle())) + .unwrap(); assert_eq!( slots .run_window(&mut window) @@ -151,12 +154,12 @@ mod tests { None ); assert_eq!(window.depth, 2); - assert!( - matches!(slots.peek(&window, 0).unwrap(), Value::Object(value) if value.object_id()==id) - ); - drop(slots.pop(&mut window).unwrap()); + assert!(matches!(slots.peek(&window, 0).unwrap(), JsValue::Object(value) if *value==id)); + runtime + .release_jsvalue(slots.pop(&mut window).unwrap()) + .unwrap(); assert!(runtime.0.state.borrow().heap.object(id).is_err()); - slots.push(&mut window, Value::Int(7)).unwrap(); + slots.push(&mut window, JsValue::Int(7)).unwrap(); assert_eq!( slots .run_window(&mut window) @@ -174,14 +177,14 @@ mod tests { .consume_number_pair(|_, _| true) .is_err() ); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } #[test] fn fused_local_result_capacity_failure_leaves_binding_and_stack_unchanged() { let runtime = Runtime::new(); let (mut slots, mut window) = frame(&runtime, 1); - slots.push(&mut window, Value::Int(99)).unwrap(); + slots.push(&mut window, JsValue::Int(99)).unwrap(); assert!( slots .run_window(&mut window) @@ -191,9 +194,9 @@ mod tests { ); assert!(matches!( slots.local(&window, 0).unwrap(), - FrameBinding::Direct(Value::Int(7)) + FrameBinding::Direct(JsValue::Int(7)) )); - assert_eq!(slots.peek(&window, 0).unwrap(), &Value::Int(99)); + assert_eq!(slots.peek(&window, 0).unwrap(), &JsValue::Int(99)); assert!( slots .run_window(&mut window) @@ -203,9 +206,9 @@ mod tests { ); assert!(matches!( slots.local(&window, 0).unwrap(), - FrameBinding::Direct(Value::Int(8)) + FrameBinding::Direct(JsValue::Int(8)) )); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(99)); + assert_eq!(slots.pop(&mut window).unwrap(), JsValue::Int(99)); assert!( slots .run_window(&mut window) @@ -213,10 +216,10 @@ mod tests { .update_number_local(0, |previous| (previous.update(false), Some(previous))) .unwrap() ); - assert_eq!(slots.pop(&mut window).unwrap(), Value::Int(8)); + assert_eq!(slots.pop(&mut window).unwrap(), JsValue::Int(8)); assert!(matches!( slots.local(&window, 0).unwrap(), - FrameBinding::Direct(Value::Int(7)) + FrameBinding::Direct(JsValue::Int(7)) )); slots .replace_local(&window, 0, FrameBinding::Uninitialized) @@ -235,6 +238,6 @@ mod tests { .update_number_local(1, |_| panic!("invalid local must not evaluate")) .is_err() ); - slots.clear_frame(window).unwrap(); + slots.clear_frame(&runtime, window).unwrap(); } } diff --git a/src/engine/vm/stack/window.rs b/src/engine/vm/stack/window.rs index ef55c36c..6ed3654f 100644 --- a/src/engine/vm/stack/window.rs +++ b/src/engine/vm/stack/window.rs @@ -1,5 +1,5 @@ //! One authenticated continuous execution borrow. No arena mutation API escapes. -use super::{Error, FrameBinding, FrameWindow, Runtime, SlotStore, Value}; +use super::{Error, FrameBinding, FrameWindow, JsValue, Runtime, SlotStore, Value}; pub(in crate::engine::vm) enum LinkedReadCompletion { Completed, @@ -20,7 +20,7 @@ pub(in crate::engine::vm) struct FrameTransaction<'a> { window: &'a mut FrameWindow, } impl FrameTransaction<'_> { - pub(in crate::engine::vm) fn peek(&self, offset: usize) -> Result<&Value, Error> { + pub(in crate::engine::vm) fn peek(&self, offset: usize) -> Result<&JsValue, Error> { self.store.peek_current(self.window, offset) } pub(in crate::engine::vm) fn validate_call_value_domains( @@ -36,6 +36,7 @@ impl FrameTransaction<'_> { } pub(in crate::engine::vm) fn take_native_call_operands( &mut self, + runtime: &Runtime, logical_active_depth: usize, count: usize, method: bool, @@ -43,7 +44,7 @@ impl FrameTransaction<'_> { self.store .reserve_native_argument_depth(logical_active_depth.saturating_add(1))?; self.store - .take_native_call_operands_current(self.window, count, method) + .take_native_call_operands_current(runtime, self.window, count, method) } /// The callback may allocate primitive storage and commit a unique String @@ -54,7 +55,7 @@ impl FrameTransaction<'_> { &mut self, left: u16, right: u16, - consume: impl FnOnce(&mut Value, &Value) -> T, + consume: impl FnOnce(&mut JsValue, &JsValue) -> T, ) -> Result, Error> { let (left, right) = (usize::from(left), usize::from(right)); let locals = &mut self.store.slots[self.window.locals()]; @@ -71,8 +72,11 @@ impl FrameTransaction<'_> { if !local_add_values(value, value) { return Ok(None); } - let right = value.clone(); - return Ok(Some(consume(value, &right))); + // Aliased locals cannot expose `&mut` and `&` views of the same + // owner to the append callback at once; decline to the canonical + // path until the fused append accepts a single-view callback. + let _ = consume; + return Ok(None); } let (left, right) = if left < right { let (before, after) = locals.split_at_mut(right); @@ -101,8 +105,8 @@ impl FrameTransaction<'_> { pub(in crate::engine::vm) fn with_local_add_constant( &mut self, left: u16, - right: &Value, - consume: impl FnOnce(&mut Value, &Value) -> T, + right: &JsValue, + consume: impl FnOnce(&mut JsValue, &JsValue) -> T, ) -> Result, Error> { let local = self.store.slots[self.window.locals()] .get_mut(usize::from(left)) @@ -123,8 +127,8 @@ impl FrameTransaction<'_> { pub(in crate::engine::vm) fn with_local_add_constant_left( &mut self, local: u16, - constant: Value, - consume: impl FnOnce(&mut Value, &Value) -> T, + constant: &mut JsValue, + consume: impl FnOnce(&mut JsValue, &JsValue) -> T, ) -> Result, Error> { let local = self.store.slots[self.window.locals()] .get(usize::from(local)) @@ -134,11 +138,10 @@ impl FrameTransaction<'_> { let FrameBinding::Direct(local) = local else { return Ok(None); }; - if !local_add_values(&constant, local) { + if !local_add_values(constant, local) { return Ok(None); } - let mut constant = constant; - Ok(Some(consume(&mut constant, local))) + Ok(Some(consume(constant, local))) } pub(in crate::engine::vm) fn slots(&mut self) -> RunSlots<'_> { RunSlots { @@ -172,7 +175,7 @@ impl SlotStore { runtime: &Runtime, executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, - complete: impl FnOnce(&mut RunSlots<'_>, &mut Option) -> Result<(), Error>, + complete: impl FnOnce(&mut RunSlots<'_>, &mut Option) -> Result<(), Error>, ) -> Result { self.with_linked_own_read_selected(window, runtime, executable, index, None, complete) } @@ -183,7 +186,7 @@ impl SlotStore { executable: &crate::engine::code::runtime::PublishedFunctionSnapshot, index: u32, native: Option<&mut Option>, - complete: impl FnOnce(&mut RunSlots<'_>, &mut Option) -> Result<(), Error>, + complete: impl FnOnce(&mut RunSlots<'_>, &mut Option) -> Result<(), Error>, ) -> Result { use crate::engine::object::OrdinaryRead; self.check_current(window)?; @@ -210,7 +213,7 @@ impl SlotStore { match selected { Some(OrdinaryRead::Complete(value)) => { // This owner stays outside the output window even on failure. - let mut value = Some(value.unwrap_or(Value::Undefined)); + let mut value = Some(value.unwrap_or(JsValue::Undefined)); { let mut slots = RunSlots { store: self, @@ -280,18 +283,18 @@ impl RunSlots<'_> { pub(in crate::engine::vm) fn depth(&self) -> usize { self.window.depth } - pub(in crate::engine::vm) fn peek(&self, from_top: usize) -> Result<&Value, Error> { + pub(in crate::engine::vm) fn peek(&self, from_top: usize) -> Result<&JsValue, Error> { self.store.peek_current(self.window, from_top) } - pub(in crate::engine::vm) fn push(&mut self, value: Value) -> Result<(), Error> { + pub(in crate::engine::vm) fn push(&mut self, value: JsValue) -> Result<(), Error> { self.store.push_current(self.window, value) } /// On failure the caller keeps its owner until this borrow has ended. pub(in crate::engine::vm) fn push_pending( &mut self, - value: &mut Option, + value: &mut Option, ) -> Result<(), Error> { self.store.push_pending_current(self.window, value) } @@ -305,7 +308,7 @@ impl RunSlots<'_> { .replace_local_pending_current(self.window, index, value) } - pub(in crate::engine::vm) fn pop(&mut self) -> Result { + pub(in crate::engine::vm) fn pop(&mut self) -> Result { self.store.pop_current(self.window) } @@ -325,10 +328,8 @@ impl RunSlots<'_> { let FrameBinding::Direct(left) = self.local(left)? else { return Ok(false); }; - Ok(!matches!(left, Value::Object(_)) - && runtime - .validate_value_domain(left, "local addition operand") - .is_ok()) + let _ = runtime; + Ok(!matches!(left, JsValue::Object(_))) } pub(in crate::engine::vm) fn local_add_supported( &self, @@ -344,6 +345,11 @@ impl RunSlots<'_> { { return Ok(false); } + // Aliased locals cannot expose `&mut` and `&` views of the same owner + // to the fused append; the canonical local-add sequence handles them. + if left == right { + return Ok(false); + } let FrameBinding::Direct(left) = self.local(left)? else { return Ok(false); }; @@ -352,13 +358,8 @@ impl RunSlots<'_> { let Ok(FrameBinding::Direct(right)) = self.local(right) else { return Ok(false); }; - let left_valid = runtime - .validate_value_domain(left, "conversion operand") - .is_ok(); - let right_valid = runtime - .validate_value_domain(right, "conversion operand") - .is_ok(); - Ok(left_valid && right_valid && local_add_values(left, right)) + let _ = runtime; + Ok(local_add_values(left, right)) } pub(in crate::engine::vm) fn local(&self, index: u16) -> Result<&FrameBinding, Error> { @@ -398,15 +399,21 @@ impl RunSlots<'_> { pub(in crate::engine::vm) fn insert_copy( &mut self, + runtime: &Runtime, source_from_top: usize, destination_from_top: usize, ) -> Result<(), Error> { self.store - .insert_copy_current(self.window, source_from_top, destination_from_top) + .insert_copy_current(runtime, self.window, source_from_top, destination_from_top) } - pub(in crate::engine::vm) fn duplicate_operands(&mut self, count: usize) -> Result<(), Error> { - self.store.duplicate_operands_current(self.window, count) + pub(in crate::engine::vm) fn duplicate_operands( + &mut self, + runtime: &Runtime, + count: usize, + ) -> Result<(), Error> { + self.store + .duplicate_operands_current(runtime, self.window, count) } pub(in crate::engine::vm) fn release_operand( @@ -432,9 +439,19 @@ impl RunSlots<'_> { keep_key: bool, ) -> Result { let index = match self.peek(0)? { - Value::Int(index) if *index >= 0 => *index as u32, - Value::String(key) if keep_key || key.release_keeps_storage_alive() => { - let Some(index) = crate::engine::atom::AtomTable::canonical_array_index(key) else { + JsValue::Int(index) if *index >= 0 => *index as u32, + JsValue::String(id) => { + if !keep_key + && !matches!( + runtime.slot_value_release_readiness_jsvalue(self.peek(0)?), + Ok(crate::engine::heap::SlotReleaseReadiness::Ready) + ) + { + return Ok(false); + } + let text = runtime.0.state.borrow().heap.string_fast(*id).clone(); + let Some(index) = crate::engine::atom::AtomTable::canonical_array_index(&text) + else { return Ok(false); }; index @@ -488,7 +505,7 @@ impl RunSlots<'_> { operation: impl FnOnce( crate::engine::value::number::operations::Number, crate::engine::value::number::operations::Number, - ) -> Value, + ) -> JsValue, ) -> Result { self.store.binary_number_current(self.window, operation) } @@ -518,11 +535,11 @@ impl RunSlots<'_> { } } -fn local_add_values(left: &Value, right: &Value) -> bool { - !matches!(left, Value::Object(_)) - && !matches!(right, Value::Object(_)) - && (matches!(left, Value::String(_) | Value::BigInt(_)) - || matches!(right, Value::String(_) | Value::BigInt(_))) +fn local_add_values(left: &JsValue, right: &JsValue) -> bool { + !matches!(left, JsValue::Object(_)) + && !matches!(right, JsValue::Object(_)) + && (matches!(left, JsValue::String(_) | JsValue::BigInt(_)) + || matches!(right, JsValue::String(_) | JsValue::BigInt(_))) } #[cfg(test)] @@ -573,11 +590,14 @@ mod primitive_transaction_tests { let mut store = SlotStore::new(8); let mut window = store .push_frame( + &runtime, &layout.frame_layout(), FrameStorage { original_arguments: vec![], parameters: vec![], - locals: vec![FrameBinding::Direct(left.clone())], + locals: vec![FrameBinding::Direct( + runtime.into_jsvalue(left.clone()).unwrap(), + )], operands: vec![], }, ) @@ -589,11 +609,11 @@ mod primitive_transaction_tests { let FrameBinding::Direct(value) = slots.local(0).unwrap() else { panic!("left") }; - let value = value.clone(); + let value = runtime.dup_jsvalue(value).unwrap(); slots.push(value).unwrap(); assert!(slots.local(u16::MAX).is_err()); assert_eq!(slots.window.depth, 1); - assert_eq!(slots.peek(0).unwrap(), &left); + assert_eq!(runtime.root_value(slots.peek(0).unwrap()).unwrap(), left); } } @@ -606,6 +626,7 @@ mod primitive_transaction_tests { let mut store = SlotStore::new(8); let mut window = store .push_frame( + &runtime, &layout.frame_layout(), FrameStorage { original_arguments: vec![], @@ -616,7 +637,12 @@ mod primitive_transaction_tests { ) .unwrap(); store - .push(&mut window, context.eval("({tag:42})").unwrap()) + .push( + &mut window, + runtime + .into_jsvalue(context.eval("({tag:42})").unwrap()) + .unwrap(), + ) .unwrap(); let mut owner; { @@ -625,19 +651,21 @@ mod primitive_transaction_tests { // No RunSlots is live during collection. The moved owner roots // the object independently of the exclusive frame transaction. runtime.run_gc().unwrap(); - transaction.slots().push(Value::Int(7)).unwrap(); + transaction.slots().push(JsValue::Int(7)).unwrap(); assert!(transaction.slots().push_pending(&mut owner).is_err()); assert!( owner.is_some(), "failed output keeps its owner outside the borrow" ); - assert_eq!(transaction.slots().pop().unwrap(), Value::Int(7)); + assert_eq!(transaction.slots().pop().unwrap(), JsValue::Int(7)); transaction.slots().push_pending(&mut owner).unwrap(); } assert!(owner.is_none()); - let Value::Object(object) = store.pop(&mut window).unwrap() else { + let popped = store.pop(&mut window).unwrap(); + let Value::Object(object) = runtime.root_value(&popped).unwrap() else { panic!("object") }; + runtime.release_jsvalue(popped).unwrap(); assert_eq!( context .get_property(&object, &runtime.intern_property_key("tag").unwrap()) @@ -656,6 +684,7 @@ mod primitive_transaction_tests { layout.metadata.max_stack = 1; let mut window = store .push_frame( + &runtime, &layout.frame_layout(), FrameStorage { original_arguments: vec![], @@ -666,7 +695,9 @@ mod primitive_transaction_tests { ) .unwrap(); let base = context.eval("({x:{tag:42}})").unwrap(); - store.push(&mut window, base).unwrap(); + store + .push(&mut window, runtime.into_jsvalue(base).unwrap()) + .unwrap(); let mut old_base = None; #[cfg(feature = "profiling")] let profile = crate::engine::api::profiling::CostProfile::start(); @@ -691,11 +722,15 @@ mod primitive_transaction_tests { ); drop(profile); } - drop(old_base); + if let Some(value) = old_base.take() { + runtime.release_jsvalue(value).unwrap(); + } runtime.run_gc().unwrap(); - let Value::Object(result) = store.pop(&mut window).unwrap() else { + let popped = store.pop(&mut window).unwrap(); + let Value::Object(result) = runtime.root_value(&popped).unwrap() else { panic!("result"); }; + runtime.release_jsvalue(popped).unwrap(); assert_eq!( context .get_property(&result, &runtime.intern_property_key("tag").unwrap()) @@ -714,6 +749,7 @@ mod primitive_transaction_tests { let mut store = SlotStore::new(8); let mut window = store .push_frame( + &runtime, &layout.frame_layout(), FrameStorage { original_arguments: vec![], @@ -730,17 +766,21 @@ mod primitive_transaction_tests { assert!( matches!(result, Err(ref error) if error.message()=="owned operand stack underflow") ); - let foreign = Runtime::new(); + // A reclaimed base handle forces the lookup path to report a lookup + // error instead of committing the operand. + let blocked = runtime.new_object(None).unwrap(); + let stale_handle = blocked.into_handle(); + runtime + .release_jsvalue(JsValue::Object(stale_handle)) + .unwrap(); + runtime.run_gc().unwrap(); store - .push( - &mut window, - Value::Object(foreign.new_object(None).unwrap()), - ) + .push(&mut window, JsValue::Object(stale_handle)) .unwrap(); assert!(matches!( store .with_linked_own_read(&mut window, &runtime, &executable, index, |_, _| panic!( - "foreign input committed" + "stale input committed" )) .unwrap(), LinkedReadCompletion::LookupError(_) @@ -758,6 +798,7 @@ mod primitive_transaction_tests { let mut store = SlotStore::new(8); let mut window = store .push_frame( + &runtime, &layout.frame_layout(), FrameStorage { original_arguments: vec![], @@ -768,7 +809,9 @@ mod primitive_transaction_tests { ) .unwrap(); let base = context.eval("globalThis.linkedCalls=0;globalThis.linkedBase={get x(){linkedCalls++;return 42}};linkedBase").unwrap(); - store.push(&mut window, base).unwrap(); + store + .push(&mut window, runtime.into_jsvalue(base).unwrap()) + .unwrap(); let LinkedReadCompletion::Pending(read) = store .with_linked_own_read(&mut window, &runtime, &executable, index, |_, _| { panic!("pending getter committed operands") @@ -804,6 +847,7 @@ mod primitive_transaction_tests { let mut store = SlotStore::new(1); let mut window = store .push_frame( + &runtime, &owner.frame_layout(), FrameStorage { original_arguments: vec![], @@ -813,10 +857,10 @@ mod primitive_transaction_tests { }, ) .unwrap(); - store.push(&mut window, Value::Int(7)).unwrap(); + store.push(&mut window, JsValue::Int(7)).unwrap(); let object = runtime.new_object(None).unwrap(); let object_id = object.object_id(); - let mut pending = Some(Value::Object(object)); + let mut pending = Some(JsValue::Object(object.into_handle())); { let mut slots = store.run_window(&mut window).unwrap(); assert!(slots.push_pending(&mut pending).is_err()); @@ -824,7 +868,7 @@ mod primitive_transaction_tests { pending.is_some(), "failure may not release the final owner inside RunSlots" ); - assert_eq!(slots.peek(0).unwrap(), &Value::Int(7)); + assert_eq!(slots.peek(0).unwrap(), &JsValue::Int(7)); } assert!(runtime.0.state.borrow().heap.object(object_id).is_ok()); let mut binding = Some(FrameBinding::Direct(pending.take().unwrap())); @@ -833,7 +877,9 @@ mod primitive_transaction_tests { assert!(slots.replace_local_pending(0, &mut binding).is_err()); assert!(binding.is_some()); } - drop(binding); + if let Some(FrameBinding::Direct(value)) = binding.take() { + runtime.release_jsvalue(value).unwrap(); + } runtime.run_gc().unwrap(); assert!(runtime.0.state.borrow().heap.object(object_id).is_err()); } diff --git a/src/engine/vm/super_property_driver.rs b/src/engine/vm/super_property_driver.rs index 5fd8d632..a861e154 100644 --- a/src/engine/vm/super_property_driver.rs +++ b/src/engine/vm/super_property_driver.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::engine::{ api::{Error, ErrorKind, runtime::Runtime}, - value::{Value, conversion::NativeConversion}, + value::{JsValue, Value, conversion::NativeConversion}, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -15,10 +15,10 @@ pub(super) enum Kind { Write, } pub(super) struct Input { - receiver: Value, - base: Value, - pub key: Value, - value: Option, + receiver: JsValue, + base: JsValue, + pub key: JsValue, + value: Option, kind: Kind, depth: usize, } @@ -40,16 +40,6 @@ pub(super) fn start( ) -> Result { let frame = execution.frames.current_mut(id)?; let realm = frame.executable.realm; - let count = if kind == Kind::Write { 4 } else { 3 }; - for offset in 0..count { - runtime - .validate_value_domain( - execution.slots.peek(&frame.window, offset)?, - "super property input", - ) - .map_err(runtime_error_to_vm_error)?; - } - let depth = execution.slots.depth(&frame.window); let value = if kind == Kind::Write { Some(execution.slots.pop(&mut frame.window)?) } else { @@ -58,12 +48,13 @@ pub(super) fn start( let key = execution.slots.pop(&mut frame.window)?; let base = execution.slots.pop(&mut frame.window)?; let receiver = execution.slots.pop(&mut frame.window)?; + let depth = execution.slots.depth(&frame.window); // PutSuperValue rejects the base before converting a raw key, after RHS. // At a call site QuickJS uses ordinary GetArrayEl's nullish precheck. - let error = if kind == Kind::Write && !matches!(base, Value::Object(_)) { + let error = if kind == Kind::Write && !matches!(base, JsValue::Object(_)) { Some("not an object") - } else if kind == Kind::Call && matches!(base, Value::Null | Value::Undefined) { - Some(if matches!(base, Value::Null) { + } else if kind == Kind::Call && matches!(base, JsValue::Null | JsValue::Undefined) { + Some(if matches!(base, JsValue::Null) { "cannot read property of null" } else { "cannot read property of undefined" @@ -87,7 +78,7 @@ pub(super) fn start( kind, depth, }); - if matches!(input.key, Value::Object(_)) { + if matches!(input.key, JsValue::Object(_)) { Ok(Progress::Convert(input)) } else { converted(runtime, execution, id, input).map(Progress::Call) @@ -112,35 +103,51 @@ pub(super) fn converted( kind, depth, } = *input; - if matches!(key, Value::Object(_)) { + if matches!(key, JsValue::Object(_)) { return Err(Error::internal("super key conversion returned an object")); } let key = match runtime - .native_to_property_key(realm, key) + .native_to_property_key_jsvalue(realm, key) .map_err(runtime_error_to_vm_error)? { NativeConversion::Value(key) => key, - NativeConversion::Throw(value) => return Ok(CallStep::Complete(Completion::Throw(value))), + NativeConversion::Throw(value) => { + return Ok(CallStep::Complete(Completion::Throw( + runtime + .into_jsvalue(value) + .map_err(runtime_error_to_vm_error)?, + ))); + } }; if kind == Kind::Write { - let Value::Object(base) = base else { + let JsValue::Object(base) = base else { return Err(Error::internal("super write lost its validated base")); }; + let base = crate::engine::object::ObjectRef::from_owned_handle(runtime.clone(), base); let value = value.ok_or_else(|| Error::internal("super write lost its value"))?; return super::proxy_get_driver::start_write( runtime, execution, id, base, key, value, receiver, strict, depth, ); } - if let Value::Object(object) = base { - let getter_receiver = if kind == Kind::Call { - Value::Object(object.clone()) - } else { - receiver.clone() - }; + if let JsValue::Object(object) = base { + let object = crate::engine::object::ObjectRef::from_owned_handle(runtime.clone(), object); if kind == Kind::Call { let frame = execution.frames.current_mut(id)?; execution.slots.push(&mut frame.window, receiver)?; + let getter_receiver = Value::Object(object.clone()); + return super::proxy_get_driver::start_owned_read( + runtime, + execution, + id, + object, + key, + getter_receiver, + depth, + ); } + let getter_receiver = runtime + .root_and_release_jsvalue(receiver) + .map_err(runtime_error_to_vm_error)?; return super::proxy_get_driver::start_owned_read( runtime, execution, @@ -153,8 +160,8 @@ pub(super) fn converted( } if kind == Kind::Read { let suffix = match base { - Value::Null => "' of null", - Value::Undefined => "' of undefined", + JsValue::Null => "' of null", + JsValue::Undefined => "' of undefined", _ => { return super::property_driver::throw_error( runtime, @@ -168,7 +175,7 @@ pub(super) fn converted( .map_err(runtime_error_to_vm_error)?; return super::property_driver::throw_error(runtime, realm, error); } - let read = runtime.prepare_value_property_read(realm, base, &key); + let read = runtime.prepare_value_property_read_borrowed_jsvalue(realm, &base, &key); let read = match read { Ok(read) => read, Err(error) => { diff --git a/src/engine/vm/suspend.rs b/src/engine/vm/suspend.rs index ff0fa8fe..e25b187d 100644 --- a/src/engine/vm/suspend.rs +++ b/src/engine/vm/suspend.rs @@ -16,7 +16,7 @@ use crate::engine::heap::{ ContextId, GeneratorActivationData, GeneratorFrameBinding, GeneratorVmActivation, RawValue, }; use crate::engine::object::{ObjectRef, PrivateNameRef}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::bindings::{FrameBinding, is_private_callable_kind}; use crate::engine::vm::call::CallableExecution; use crate::engine::vm::frames::ActiveFrameToken; @@ -28,14 +28,21 @@ mod owned; pub(super) use owned::{OwnedSuspension, PreparedResume}; +/// Reconstruct one owned internal value from a dormant heap record, retaining +/// every edge so the decoded value owns them independently of the record. +fn decode_raw_jsvalue(runtime: &Runtime, raw: &RawValue) -> Result { + let value = JsValue::from_raw(raw.clone()).ok_or(RuntimeError::Invariant( + "dormant activation held an internal-only sentinel", + ))?; + runtime.dup_jsvalue(&value) +} + fn encode_generator_frame_binding( runtime: &Runtime, binding: &FrameBinding, ) -> Result { Ok(match binding { - FrameBinding::Direct(value) => { - GeneratorFrameBinding::Direct(runtime.raw_property_value(value)?) - } + FrameBinding::Direct(value) => GeneratorFrameBinding::Direct(value.as_raw()), FrameBinding::Private(name) => { if !name.belongs_to(runtime) { return Err(RuntimeError::WrongRuntime("generator private binding")); @@ -124,7 +131,7 @@ fn decode_generator_frame_binding( ) -> Result { let binding = match binding { GeneratorFrameBinding::Direct(value) => { - FrameBinding::Direct(runtime.root_raw_value(value)?) + FrameBinding::Direct(decode_raw_jsvalue(runtime, value)?) } GeneratorFrameBinding::Private(atom) => { if runtime.0.state.borrow().atoms.kind(*atom)? != AtomKind::Private { @@ -159,49 +166,96 @@ fn decode_generator_frame_binding( pub(crate) struct EncodedVmActivation { pub(crate) kind: VmSuspendKind, pub(crate) data: GeneratorActivationData, - _entry: super::frame::FrameEntry, + _entry: EncodedActivationEntry, } impl EncodedVmActivation { - pub(crate) fn atoms(&self) -> Vec { + /// Brand every retained atom index for the caller's explicit retain pass. + /// `GeneratorFrameBinding::Private` already carries a branded boundary + /// `Atom` and passes through untouched. + pub(crate) fn atoms( + &self, + table: &crate::engine::atom::AtomTable, + ) -> Result, RuntimeError> { let vm = &self.data.vm; - vm.stack + let mut atoms = Vec::new(); + for value in vm + .stack .iter() .chain(self.data.original_arguments.iter()) .chain(std::iter::once(&vm.this_value)) .chain(vm.normalized_this.iter()) .chain(std::iter::once(&vm.new_target)) - .filter_map(generator_raw_value_atom) - .chain( - self.data - .arguments - .iter() - .chain(self.data.locals.iter()) - .filter_map(|binding| match binding { - GeneratorFrameBinding::Direct(value) => generator_raw_value_atom(value), - GeneratorFrameBinding::Private(atom) => Some(*atom), - GeneratorFrameBinding::PrivateCallable(_) - | GeneratorFrameBinding::Uninitialized - | GeneratorFrameBinding::Captured(_) => None, - }), - ) - .collect() + { + if let RawValue::Symbol(index) | RawValue::Private(index) = value { + atoms.push(table.brand(*index)?); + } + } + for binding in self.data.arguments.iter().chain(self.data.locals.iter()) { + match binding { + GeneratorFrameBinding::Direct(value) => { + if let RawValue::Symbol(index) | RawValue::Private(index) = value { + atoms.push(table.brand(*index)?); + } + } + GeneratorFrameBinding::Private(atom) => atoms.push(*atom), + GeneratorFrameBinding::PrivateCallable(_) + | GeneratorFrameBinding::Uninitialized + | GeneratorFrameBinding::Captured(_) => {} + } + } + Ok(atoms) + } + + /// Release the caller-owned string/BigInt producer edge carried by every + /// boundary-converted raw value, once the heap owner has retained its own + /// copies (or immediately when the activation is never stored). + pub(crate) fn release_conversion_edges(&mut self, runtime: &Runtime) { + let vm = &self.data.vm; + for value in vm + .stack + .iter() + .chain(self.data.original_arguments.iter()) + .chain(std::iter::once(&vm.this_value)) + .chain(vm.normalized_this.iter()) + .chain(std::iter::once(&vm.new_target)) + { + runtime.release_converted_value_edge(value); + } + for binding in self.data.arguments.iter().chain(self.data.locals.iter()) { + if let GeneratorFrameBinding::Direct(value) = binding { + runtime.release_converted_value_edge(value); + } + } } } -fn generator_raw_value_atom(value: &RawValue) -> Option { - match value { - RawValue::Symbol(atom) | RawValue::Private(atom) => Some(*atom), - RawValue::Undefined - | RawValue::Null - | RawValue::Bool(_) - | RawValue::Int(_) - | RawValue::Float(_) - | RawValue::BigInt(_) - | RawValue::String(_) - | RawValue::Object(_) - | RawValue::Uninitialized - | RawValue::Exception => None, +/// Owns the source frame entry across heap publication and releases the +/// remaining caller-owned object/symbol/binding edges when the activation is +/// finally abandoned. Direct String/BigInt edges are the boundary-conversion +/// producer edges already released through `release_conversion_edges`, so the +/// storage release skips them. The drop runs after any state borrow has been +/// released, keeping releases nothrow. +struct EncodedActivationEntry { + runtime: Runtime, + entry: Option, +} + +impl Drop for EncodedActivationEntry { + fn drop(&mut self) { + let Some(mut entry) = self.entry.take() else { + return; + }; + let storage = std::mem::replace( + &mut entry.storage, + super::stack::FrameStorage { + original_arguments: Vec::new(), + parameters: Vec::new(), + locals: Vec::new(), + operands: Vec::new(), + }, + ); + super::stack::release_unconverted_frame_storage(&self.runtime, storage); } } @@ -217,14 +271,14 @@ pub(crate) struct RootedVmActivation { pub(crate) enum VmActivationResume { Initial, Generator(VmResume), - AwaitFulfill(Value), - AwaitReject(Value), + AwaitFulfill(JsValue), + AwaitReject(JsValue), } pub(crate) enum VmRunOutcome { Complete(Completion), Suspend { - value: Value, + value: JsValue, activation: Box, }, } @@ -241,15 +295,14 @@ impl RootedVmActivation { if self.entry.cold.function.runtime().domain_id() != runtime.domain_id() { return Err(RuntimeError::WrongRuntime("suspended execution")); } + // Internal resume values are handle-only and carry no runtime tag, so + // their domain is guaranteed by the state machine that produced them; + // only the activation's own runtime is authenticated here. match resume { VmActivationResume::Initial => {} - VmActivationResume::Generator( - VmResume::Next(value) | VmResume::Return(value) | VmResume::Throw(value), - ) - | VmActivationResume::AwaitFulfill(value) - | VmActivationResume::AwaitReject(value) => { - runtime.validate_value_domain(value, "suspension resume value")?; - } + VmActivationResume::Generator(_) + | VmActivationResume::AwaitFulfill(_) + | VmActivationResume::AwaitReject(_) => {} } Ok(()) } @@ -295,7 +348,7 @@ impl RootedVmActivation { BytecodePc::new(saved_pc.saturating_sub(1)), )?; entry.cold.entry_guard = Some(guard); - owned::prepare(entry, kind, saved_pc, resume) + owned::prepare(runtime, entry, kind, saved_pc, resume) } } @@ -337,20 +390,20 @@ pub(super) fn freeze_entry( stack: storage .operands .iter() - .map(|value| runtime.raw_property_value(value)) - .collect::, _>>()?, + .map(|value| value.as_raw()) + .collect(), regions: entry.cold.regions.clone(), pc, callee_realm: entry.executable.realm, current_function: entry.cold.function.object_id(), - this_value: runtime.raw_property_value(&input.this_value)?, + this_value: input.this_value.as_raw(), normalized_this: entry .cold .normalized_this .as_ref() .map(|value| runtime.raw_property_value(value)) .transpose()?, - new_target: runtime.raw_property_value(&input.new_target)?, + new_target: input.new_target.as_raw(), strict: entry.executable.frame_layout().is_strict(), callee_global: global.object_id(), }; @@ -363,13 +416,16 @@ pub(super) fn freeze_entry( original_arguments: storage .original_arguments .iter() - .map(|value| runtime.raw_property_value(value)) - .collect::, _>>()?, + .map(|value| value.as_raw()) + .collect(), arguments, locals, reusable_captured_locals: entry.cold.reusable_captured_locals.clone(), }, - _entry: entry, + _entry: EncodedActivationEntry { + runtime: runtime.clone(), + entry: Some(entry), + }, }) } @@ -434,44 +490,69 @@ pub(crate) fn thaw( "resumable closure slot count disagrees with bytecode metadata", )); } - let original_arguments = data - .original_arguments - .iter() - .map(|value| runtime.root_raw_value(value)) - .collect::, _>>()?; - let arguments = data - .arguments - .iter() - .enumerate() - .map(|(index, binding)| { - decode_generator_frame_binding(&runtime, binding, argument_definitions.get(index)) - }) - .collect::, _>>()?; - let locals = data - .locals - .iter() - .zip(local_definitions.iter()) - .map(|(binding, definition)| { - decode_generator_frame_binding(&runtime, binding, Some(definition)) - }) - .collect::, _>>()?; let callee_global = ObjectRef::from_borrowed_handle(runtime.clone(), data.vm.callee_global)?; - let operands = data - .vm - .stack - .iter() - .map(|value| runtime.root_raw_value(value)) - .collect::, _>>()?; - if kind != VmSuspendKind::Initial && !matches!(operands.last(), Some(Value::Undefined)) { + // Decode incrementally into an owning guard: a later rejection releases + // every root already reconstructed instead of leaking the partial frame. + let mut roots = super::stack::FrameStorageGuard::new( + &runtime, + super::stack::FrameStorage { + original_arguments: Vec::new(), + parameters: Vec::new(), + locals: Vec::new(), + operands: Vec::new(), + }, + ); + { + let storage = roots.storage_mut(); + for value in &data.original_arguments { + storage + .original_arguments + .push(decode_raw_jsvalue(&runtime, value)?); + } + for (index, binding) in data.arguments.iter().enumerate() { + storage.parameters.push(decode_generator_frame_binding( + &runtime, + binding, + argument_definitions.get(index), + )?); + } + for (binding, definition) in data.locals.iter().zip(local_definitions.iter()) { + storage.locals.push(decode_generator_frame_binding( + &runtime, + binding, + Some(definition), + )?); + } + for value in &data.vm.stack { + storage.operands.push(decode_raw_jsvalue(&runtime, value)?); + } + } + if kind != VmSuspendKind::Initial + && !matches!( + roots.storage_mut().operands.last(), + Some(JsValue::Undefined) + ) + { return Err(RuntimeError::Invariant( "dormant suspension output was not cleared", )); } - let input = super::CallInput { - this_value: runtime.root_raw_value(&data.vm.this_value)?, - new_target: runtime.root_raw_value(&data.vm.new_target)?, - callee_global: Some(callee_global), + let this_value = decode_raw_jsvalue(&runtime, &data.vm.this_value)?; + let new_target = match decode_raw_jsvalue(&runtime, &data.vm.new_target) { + Ok(value) => value, + Err(error) => { + let _ = runtime.release_jsvalue(this_value); + return Err(error); + } }; + let input = super::CallInput::new(&runtime, this_value, new_target, Some(callee_global)); + let normalized_this = data + .vm + .normalized_this + .as_ref() + .map(|value| runtime.root_raw_value(value)) + .transpose()?; + let storage = roots.take(); let mut entry = super::frame::FrameEntry { initialize_bindings: false, property_generation: 0, @@ -488,20 +569,10 @@ pub(crate) fn thaw( reusable_captured_locals: data.reusable_captured_locals.clone(), input: input.into(), }), - storage: super::stack::FrameStorage { - original_arguments, - parameters: arguments, - locals, - operands, - }, + storage, }; entry.cold.regions = data.vm.regions.clone(); - entry.cold.normalized_this = data - .vm - .normalized_this - .as_ref() - .map(|value| runtime.root_raw_value(value)) - .transpose()?; + entry.cold.normalized_this = normalized_this; Ok(RootedVmActivation { entry, kind, @@ -514,6 +585,7 @@ mod tests { use super::*; use crate::engine::api::Context; use crate::engine::heap::GeneratorState; + use crate::engine::value::Value; fn dormant(context: &mut Context) -> (ObjectRef, GeneratorActivationData) { let Value::Object(generator) = context @@ -580,7 +652,12 @@ mod tests { .generator_snapshot(generator.object_id()) .unwrap(); assert_eq!(state, GeneratorState::SuspendedStart); - assert_eq!(after.as_ref(), Some(&data)); + // `GeneratorActivationData` has no `PartialEq` (its VM fields embed + // `RawValue`), so equality is checked through the debug rendering. + assert_eq!( + after.as_ref().map(|entry| format!("{entry:?}")), + Some(format!("{:?}", data)) + ); assert!(runtime.0.state.borrow().active_frames.is_empty()); } @@ -609,34 +686,23 @@ mod tests { } #[test] - fn resume_rejects_foreign_runtime_and_values_before_registering_a_frame() { + fn resume_rejects_foreign_runtime_before_registering_a_frame() { let runtime = Runtime::new(); let other = Runtime::new(); let mut context = runtime.new_context(); let (_generator, data) = dormant(&mut context); - for foreign_runtime in [true, false] { - let rooted = thaw( - runtime.clone(), - VmSuspendKind::Initial, - context.realm, - &data, - FunctionKind::Generator, - ) - .unwrap(); - let result = if foreign_runtime { - rooted.run(&other, VmActivationResume::Initial) - } else { - rooted.run( - &runtime, - VmActivationResume::Generator(VmResume::Next(Value::Object( - other.new_object(None).unwrap(), - ))), - ) - }; - assert!(matches!(result, Err(RuntimeError::WrongRuntime(_)))); - assert!(runtime.0.state.borrow().active_frames.is_empty()); - assert!(other.0.state.borrow().active_frames.is_empty()); - } + let rooted = thaw( + runtime.clone(), + VmSuspendKind::Initial, + context.realm, + &data, + FunctionKind::Generator, + ) + .unwrap(); + let result = rooted.run(&other, VmActivationResume::Initial); + assert!(matches!(result, Err(RuntimeError::WrongRuntime(_)))); + assert!(runtime.0.state.borrow().active_frames.is_empty()); + assert!(other.0.state.borrow().active_frames.is_empty()); } } diff --git a/src/engine/vm/suspend/creation.rs b/src/engine/vm/suspend/creation.rs index fd6ba0c0..6ad10778 100644 --- a/src/engine/vm/suspend/creation.rs +++ b/src/engine/vm/suspend/creation.rs @@ -76,7 +76,7 @@ impl GeneratorPrototype { Completion::Throw(value) => { return Ok(CreationStep::Complete(Completion::Throw(value))); } - Completion::Return(value) => value, + Completion::Return(value) => runtime.root_and_release_jsvalue(value)?, }; let prototype = if let Value::Object(prototype) = value { prototype @@ -85,7 +85,9 @@ impl GeneratorPrototype { match runtime.function_realm(self.creation.realm, &self.creation.callable)? { NativeConversion::Value(realm) => realm, NativeConversion::Throw(value) => { - return Ok(CreationStep::Complete(Completion::Throw(value))); + return Ok(CreationStep::Complete(Completion::Throw( + runtime.into_jsvalue(value)?, + ))); } }; let id = { @@ -114,9 +116,9 @@ impl GeneratorPrototype { } else { runtime.allocate_generator_object(&prototype, *self.activation)? }; - Ok(CreationStep::Complete(Completion::Return(Value::Object( - generator, - )))) + Ok(CreationStep::Complete(Completion::Return( + runtime.into_jsvalue(Value::Object(generator))?, + ))) } } diff --git a/src/engine/vm/suspend/owned.rs b/src/engine/vm/suspend/owned.rs index e87679f0..3aa0723b 100644 --- a/src/engine/vm/suspend/owned.rs +++ b/src/engine/vm/suspend/owned.rs @@ -1,7 +1,7 @@ //! Move a suspended owned frame across the heap-publication boundary. use super::{VmActivationResume, VmRunOutcome}; use crate::engine::api::{Error, runtime::Runtime, runtime_error::RuntimeError}; -use crate::engine::value::Value; +use crate::engine::value::JsValue; use crate::engine::vm::execution::RunningExecution; use crate::engine::vm::frame::{FrameEntry, FrameId}; use crate::engine::vm::{VmResume, VmSuspendKind}; @@ -41,7 +41,7 @@ impl OwnedSuspension { )); } let mut frame = execution.frames.pop(id)?; - let storage = execution.slots.take_frame(frame.window.take())?; + let storage = execution.slots.take_frame(&runtime, frame.window.take())?; if let Some(guard) = frame.cold.entry_guard.take() { guard .finish() @@ -80,7 +80,7 @@ impl OwnedSuspension { } = *self; let mut entry = entry; let value = if kind == VmSuspendKind::Initial { - Value::Undefined + JsValue::Undefined } else { std::mem::replace( entry @@ -88,7 +88,7 @@ impl OwnedSuspension { .operands .last_mut() .ok_or(RuntimeError::Invariant("suspension has no output operand"))?, - Value::Undefined, + JsValue::Undefined, ) }; Ok(VmRunOutcome::Suspend { @@ -99,6 +99,7 @@ impl OwnedSuspension { } pub(super) fn prepare( + runtime: &Runtime, mut entry: FrameEntry, kind: VmSuspendKind, pc: usize, @@ -131,7 +132,7 @@ pub(super) fn prepare( } }; if kind != VmSuspendKind::Initial - && !matches!(entry.storage.operands.last(), Some(Value::Undefined)) + && !matches!(entry.storage.operands.last(), Some(JsValue::Undefined)) { return Err(RuntimeError::Invariant( "suspension resume operand was not cleared", @@ -149,10 +150,13 @@ pub(super) fn prepare( .operands .try_reserve(1) .map_err(|_| RuntimeError::Invariant("resume operand allocation failed"))?; - entry.storage.operands.push(Value::Int(magic)); + entry.storage.operands.push(JsValue::Int(magic)); } } - entry.cold.resume_throw = abrupt; + entry.cold.resume_throw = match abrupt { + Some(value) => Some(runtime.root_and_release_jsvalue(value)?), + None => None, + }; Ok(PreparedResume { entry, pc }) }