From 263c7cb92609fb4669d54018bb6158e783c8d29d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 17:10:48 -0800 Subject: [PATCH 1/3] JavaScript: arguments from apply stay alive while a native function runs A native function called through apply could run JavaScript that started a collection, which freed the arguments apply had read from the array-like object. The native call now roots its receiver and arguments. The parsing entry in docs/target-app-parity.md no longer says that nothing runs scripts. --- crates/js/src/interp.rs | 11 ++++++++++- crates/js/src/interp_tests.rs | 13 +++++++++++++ docs/target-app-parity.md | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 7df501699..1740c5733 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -1508,7 +1508,16 @@ impl Interp { this = args.first().cloned().unwrap_or(Value::Undefined); args = self.list_from_array_like(&list)?; } - Func::Native { f: native, .. } => return Ok(Prepared::Done(native(self, this, &args, false)?)), + Func::Native { f: native, .. } => { + // A list from `apply` is not on the operand stack, and a native function can run + // JavaScript that collects, so the arguments stay rooted for the call. + let mark = self.temp_roots.len(); + self.temp_roots.push(this.clone()); + self.temp_roots.extend(args.iter().cloned()); + let r = native(self, this, &args, false); + self.temp_roots.truncate(mark); + return Ok(Prepared::Done(r?)); + } Func::Host { f: host, .. } => { let js_args: Vec = args.iter().map(|v| self.to_js_value(v)).collect(); return match host(&js_args) { diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index 30c84d857..6c2608f2b 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -456,6 +456,19 @@ fn a_lower_heap_limit_applies_to_what_is_already_live() { assert_eq!(limit(&mut realm, "var more = []; for (var j = 0; j < 20000; j++) more[j] = {j: j};"), LimitKind::Heap); } +#[test] +fn apply_keeps_its_arguments_alive_while_a_native_function_runs() { + // The first argument's toString makes 20,000 objects, more than a 1 MiB heap holds, so a + // collection runs before it returns. The second argument is reachable only from the list + // that apply read from the array-like object. + let mut realm = small(Limits { heap_bytes: 1 << 20, ..Limits::default() }); + let src = "var list = {length: 2, \ + get 0() { return {toString: function () { for (var i = 0; i < 20000; i++) ({i: i}); return 'a'; }}; }, \ + get 1() { return {toString: function () { return 'return a * 2'; }}; }}; \ + Function.apply(null, list)(21)"; + assert_eq!(realm.run(src), Ok(JsValue::Number(42.0))); +} + #[test] fn to_number_skips_the_white_space_the_lexer_skips() { let mut realm = small(Limits::default()); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 27e8b3d79..ccc436f7c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -653,7 +653,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `window.panel` ids are case-insensitive (display labels work too), `vectorcraft://object/{id}` reads through `document.node {summary: true}` (the same reply bytes from one lookup instead of summarizing the whole document: a leaf read on a 2,000-object document drops from ~20 ms to ~0.06 ms), large containers slice with `document.node {depth?, childLimit?}` (truncated levels report `childCount`), `document.inspect` and the `inspect_document` tool slice the same way so a depth-0 read is the layer skeleton (~9.7 KB instead of ~2 MB on a 2,000-object document), `document.find` locates nodes by name/kind/text with ancestor paths, and the CLI sweeps the protocol surface over stdio against the compiled binary. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads). - Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`). A `run` step, like a `command_batch` step over MCP, can use an earlier step's result (`"$1.id"`, #785); `run --commands FILE` (or `-` for standard input) reads the steps from a JSON array or JSON Lines, past the Windows command-line limit (#3147, `apps/vectorcraft-cli/tests/it/cli.rs` `run_reads_*`, `run_refuses_malformed_commands_naming_the_step`). - File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files. - - `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). Nothing runs scripts yet. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`. + - `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). This entry covers parsing; the next entry covers running scripts. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`. - `vectorcraft-js`'s `Realm` runs ECMAScript 5.1 scripts: the parsed script compiles to verified bytecode that a non-recursive interpreter runs, with closures, `this`, `arguments`, `with`, direct and indirect `eval`, `new Function`, `try`/`catch`/`finally`, labeled `break` and `continue`, and `for`-`in` in insertion order, over a mark-and-sweep heap. Built-ins are limited to `Object`, `Function` with `call` and `apply`, `eval` and the error types. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag (smaller defaults in the wasm build), and a run that reaches a limit stops with a limit error that `try` cannot catch. `cargo xtask test262` also runs the suite's ES5 language tests that need only these built-ins: 3,074 of 3,078 runs pass (the 4 others expect the completion values of later editions). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`. - **UI:** Illustrator 2026 layout restyled to measured values: - Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs. From aa3bcc4c77347800d0c88dd179d8bce364bbc444 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 17:22:26 -0800 Subject: [PATCH 2/3] JavaScript: each NativeError prototype is an Error object Object.prototype.toString gives "[object Error]" for TypeError.prototype and the other NativeError prototypes, as ECMAScript 5.1 section 15.11.7.7 defines them. Before, only Error.prototype had the Error class. --- crates/js/src/builtins.rs | 7 +++---- crates/js/src/interp_tests.rs | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index b16aa9c9e..3eda7b6a6 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -113,12 +113,11 @@ fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> { ]; let mut error_proto = object_proto; for (i, (name, f)) in kinds.into_iter().enumerate() { - let proto = alloc(interp, Class::Ordinary, Some(if i == 0 { object_proto } else { error_proto })); + // Error.prototype and each NativeError prototype are Error objects (sections 15.11.4 and + // 15.11.7.7). + let proto = alloc(interp, Class::Error, Some(if i == 0 { object_proto } else { error_proto })); if i == 0 { error_proto = proto; - if let Some(p) = interp.heap.object_mut(proto) { - p.class = Class::Error; - } method(interp, proto, "toString", error_to_string, 0)?; } if let Some(slot) = interp.intrinsics.error_protos.get_mut(i) { diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index 6c2608f2b..8dff13050 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -118,6 +118,11 @@ fn objects_arrays_and_accessors() { truth("'abc'.length === 3 && 'abc'[1] === 'b' && 'abc'.x === undefined"); truth("var o = {}; o.hasOwnProperty('x') === false && ({x: 1}).hasOwnProperty('x')"); truth("Object.prototype.toString.call([]) === '[object Array]' && Object.prototype.toString.call(null) === '[object Null]'"); + // Error.prototype and each NativeError prototype are Error objects (sections 15.11.4 and 15.11.7.7). + truth( + "var s = Object.prototype.toString, all = [Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError], ok = true;\ + for (var i = 0; i < all.length; i++) ok = ok && s.call(all[i].prototype) === '[object Error]'; ok", + ); let e = fail("var a = []; a.length = -1"); assert!(e.message.starts_with("RangeError"), "{}", e.message); let e = fail("'use strict'; var o = { get x() { return 1; } }; o.x = 2;"); From c7baa679d91449bb9be925316f4851ec3e80209d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 19:02:53 -0800 Subject: [PATCH 3/3] JavaScript: apply takes fuel for each argument it reads Function.prototype.apply reads up to 65,536 elements from an array-like object. Each element read now takes a unit of fuel, so a long list ends the run with the fuel limit as other loops in the interpreter do. --- crates/js/src/interp.rs | 3 ++- crates/js/src/interp_tests.rs | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 1740c5733..9d16046d0 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -1550,7 +1550,8 @@ impl Interp { let saved = self.temp_roots.len(); let mut out = Vec::with_capacity(n); for i in 0..n { - match self.get_from(*o, &index_key(u32::try_from(i).unwrap_or(u32::MAX)), list) { + // Each element read takes fuel, so a long array-like list stays within the run's limits. + match self.use_fuel().and_then(|()| self.get_from(*o, &index_key(u32::try_from(i).unwrap_or(u32::MAX)), list)) { Ok(v) => { self.temp_roots.push(v.clone()); out.push(v); diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index 8dff13050..03d9e6639 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -391,6 +391,13 @@ fn shrinking_an_array_takes_fuel_for_each_property() { assert_eq!(realm.run("a.length = 1; a.length + a[0] + (99999 in a ? 1 : 0)"), Ok(JsValue::Number(1.0))); } +#[test] +fn apply_takes_fuel_for_each_argument_it_reads() { + let mut realm = small(Limits { fuel: 1000, ..Limits::default() }); + assert_eq!(limit(&mut realm, "(function () {}).apply(null, {length: 65536});"), LimitKind::Fuel); + assert_eq!(realm.run("(function () { return arguments.length; }).apply(null, {length: 100})"), Ok(JsValue::Number(100.0))); +} + #[test] fn declarations_check_the_heap_before_they_are_bound() { // Each script declares more than fits in the heap. The run ends with the heap limit before