Merge feat/js-interpreter into feat/js-builtins-core

Takes #6178's rooting of every native call's arguments, the fuel per argument apply reads and the Error objects as NativeError prototypes. The bound-function and apply arm that rooted arguments only for those calls is removed, since every native call now roots them; the parsing line in docs/target-app-parity.md takes #6178's wording.
This commit is contained in:
dotCooCoo
2026-11-07 19:12:18 -08:00
4 changed files with 41 additions and 17 deletions
+3 -4
View File
@@ -271,12 +271,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) {
+12 -12
View File
@@ -1691,9 +1691,6 @@ impl Interp {
/// Resolves `call` and `apply` and runs native and host functions; a function written in
/// JavaScript comes back to be entered by the caller.
fn prepare_call(&mut self, mut f: Value, mut this: Value, mut args: Vec<Value>) -> JsResult<Prepared> {
// The arguments `apply` reads and those a bound function adds are not on the operand
// stack, so a native function's collection must be told about them.
let mut unrooted = false;
loop {
let Some(func) = self.func_of(&f) else { return Err(self.type_error("the value is not a function")) };
match func {
@@ -1715,7 +1712,6 @@ impl Interp {
f = this;
this = args.first().cloned().unwrap_or(Value::Undefined);
args = self.list_from_array_like(&list)?;
unrooted = true;
}
Func::Bound { target, this: bound_this, args: bound_args } => {
self.use_fuel()?;
@@ -1725,15 +1721,18 @@ impl Interp {
args = all;
this = bound_this;
f = Value::Object(target);
unrooted = true;
}
Func::Native { f: native, .. } if unrooted => {
let mut roots = args.clone();
roots.push(this.clone());
let v = self.rooted(&roots, |interp| native(interp, this, &args, false))?;
return Ok(Prepared::Done(v));
Func::Native { f: native, .. } => {
// A list from `apply` and the arguments a bound function adds are 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::Native { f: native, .. } => return Ok(Prepared::Done(native(self, this, &args, false)?)),
Func::Host { f: host, .. } => {
let js_args: Vec<crate::JsValue> = args.iter().map(|v| self.to_js_value(v)).collect();
return match host(&js_args) {
@@ -1766,7 +1765,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);
+25
View File
@@ -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;");
@@ -386,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
@@ -456,6 +468,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());
+1 -1
View File
@@ -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, with the built-ins the next entry lists. 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 the built-ins present: 3,362 of 3,387 runs pass (4 of the others expect the completion values of later editions, and 21 call `String` or `Number` methods). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`.
- `vectorcraft-js` provides the ECMAScript 5.1 built-ins `Object` with its property-descriptor functions, `create`, `keys`, `seal` and `freeze` (section 15.2), `Function.prototype.bind` (15.3.4.5), `Array` with every ES5 method (15.4), the error types (15.11), `Math` (15.8), `JSON` with revivers, replacers and indentation (15.12), and the global functions with Annex B's `escape` and `unescape` (15.1, B.2). The array methods work on array-like objects and visit only the indices that exist, so `new Array(4294967295).join("")` ends at once; `join` writes a cycle back to an array as an empty string, `JSON.stringify` throws a `TypeError` on a cycle, and deep `JSON` nesting uses no native stack. Every loop takes fuel and every result is checked against the heap limit. `cargo xtask test262` runs the suite's ES5 built-in tests that need only these built-ins: 4,478 of 4,556 runs pass (16 of the failing tests call `String`, `Number` or `Boolean` methods, and 23 expect rules of later editions). It also fetches the `json2.js` polyfill at a pinned commit to compare with the built-in `JSON`; that test runs once `Date`, `String.prototype.replace` and `RegExp` exist. Tests: `crates/js/src/builtins_tests.rs`, `crates/js/tests/test262.rs`, `crates/js/tests/json2.rs`.
- **UI:** Illustrator 2026 layout restyled to measured values: