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

This commit is contained in:
dotCooCoo
2026-11-07 19:34:34 -08:00
4 changed files with 41 additions and 7 deletions
+3 -4
View File
@@ -118,12 +118,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 -2
View File
@@ -1558,7 +1558,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<crate::JsValue> = args.iter().map(|v| self.to_js_value(v)).collect();
return match host(&js_args) {
@@ -1591,7 +1600,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. 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`.
- `vectorcraft-js` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential`, `toPrecision` and `toString(radix)` format a number from its exact decimal value. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher that keeps its choice points on the heap and takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,113 of 1,169 pass for `String`, 262 of 262 for `Number`, 878 of 908 for `RegExp` and 6 of 6 for `Date`. The others need `Array` and `Boolean` methods, or expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object). The ES5 language tests that can now run give 3,742 of 3,746 passing runs. Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.rs`.
- **UI:** Illustrator 2026 layout restyled to measured values: