diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 4fc1adc87..0e437c961 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -53,12 +53,17 @@ fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId, proto: ObjI data(interp, global, name, Value::Object(ctor), HIDDEN) } -/// Creates the built-ins and the global object's properties. +/// Creates the built-ins and the global object's properties. Installing is not bounded by +/// [`crate::Limits`]; if it fails, the failure is kept in `interp.install_error` and every run of +/// the realm ends with it. pub(crate) fn install(interp: &mut Interp) { if let Err(e) = try_install(interp) { - // Installing touches only objects it has just made, so this does not happen; a realm - // that somehow lacks a built-in reports it when a script uses it. - let _ = e; + let reason = match e { + Abrupt::Throw(v) => interp.describe(&v), + Abrupt::Limit(kind) => format!("limit reached: {kind:?}"), + Abrupt::Internal(message) => message, + }; + interp.install_error = Some(format!("the built-ins could not be installed: {reason}")); } } diff --git a/crates/js/src/builtins/json.rs b/crates/js/src/builtins/json.rs index 8061379ce..b84d9dc7b 100644 --- a/crates/js/src/builtins/json.rs +++ b/crates/js/src/builtins/json.rs @@ -16,7 +16,6 @@ use crate::interp::{Desc, ErrorType, Hint, Interp, JsResult}; use crate::value::{Str, Value, number_to_str}; pub(super) fn install(interp: &mut Interp) -> JsResult<()> { - interp.reserve_heap(OBJECT_BYTES)?; let proto = interp.intrinsics.object_proto; let json = interp.heap.alloc_object(Object::new(Class::Json, Some(proto))); method(interp, json, "parse", parse, 2)?; diff --git a/crates/js/src/builtins/math.rs b/crates/js/src/builtins/math.rs index 3ad9421ed..08fe96a04 100644 --- a/crates/js/src/builtins/math.rs +++ b/crates/js/src/builtins/math.rs @@ -2,12 +2,11 @@ //! cases the section lists for NaN, signed zeros and infinities. use super::{arg, data, method}; -use crate::heap::{Class, NativeFn, OBJECT_BYTES, Object}; +use crate::heap::{Class, NativeFn, Object}; use crate::interp::{Interp, JsResult}; use crate::value::Value; pub(super) fn install(interp: &mut Interp) -> JsResult<()> { - interp.reserve_heap(OBJECT_BYTES)?; let proto = interp.intrinsics.object_proto; let math = interp.heap.alloc_object(Object::new(Class::Math, Some(proto))); let constants = [ diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index 4a4c2aea4..7261054b7 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -356,6 +356,31 @@ fn define_properties_checks_the_heap_before_each_new_property() { assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes); } +#[test] +fn a_heap_limit_smaller_than_the_built_ins_still_installs_every_built_in() { + // The heap limit bounds what scripts make, not the realm's own built-ins: a realm made with a + // tiny limit refuses runs with the heap limit, and has every built-in once the limit is raised. + let mut realm = small(Limits { heap_bytes: 1, ..Limits::default() }); + assert_eq!(limit(&mut realm, "[]"), LimitKind::Heap); + realm.set_limits(Limits::default()); + assert_eq!( + realm.run("typeof Math.max + typeof JSON.stringify + typeof parseInt + typeof escape + typeof decodeURIComponent"), + Ok(JsValue::String("functionfunctionfunctionfunctionfunction".into())) + ); +} + +#[test] +fn a_realm_whose_built_ins_failed_to_install_runs_nothing() { + let mut realm = Realm::new(); + realm.interp().install_error = Some("the built-ins could not be installed: test".into()); + match realm.run("var ran = true; 1") { + Err(RunError { kind: RunErrorKind::Internal, message, .. }) => assert_eq!(message, "the built-ins could not be installed: test"), + other => panic!("{other:?}"), + } + realm.interp().install_error = None; + assert_eq!(realm.run("typeof ran"), Ok(JsValue::String("undefined".into()))); +} + #[test] fn number_parsing_counts_its_copy_toward_the_heap_limit() { // `s` is 1 Mi digits, so parsing it in base 10 copies 1 MiB, which does not fit in the diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 7168f9dad..32ab99275 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -170,6 +170,8 @@ pub(crate) struct Interp { /// charged to `heap.bytes` and added back after each collection, and a native call's /// buffers are released when it returns. pub(crate) held_bytes: usize, + /// Why installing the built-ins failed; a realm with this set runs no script. + pub(crate) install_error: Option, } /// The least heap size a collection waits for. @@ -307,6 +309,7 @@ impl Interp { random: random_seed(), join_stack: Vec::new(), held_bytes: 0, + install_error: None, }; crate::builtins::install(&mut interp); interp.gc_threshold = interp.next_threshold(); diff --git a/crates/js/src/realm.rs b/crates/js/src/realm.rs index f95d57d17..18f39d96b 100644 --- a/crates/js/src/realm.rs +++ b/crates/js/src/realm.rs @@ -170,9 +170,13 @@ impl Realm { } /// Runs `source` as a script in the global environment and returns its completion value (the - /// value of the last expression statement that ran). + /// value of the last expression statement that ran). A realm whose built-ins could not be + /// installed runs nothing and gives an internal error. pub fn run(&mut self, source: &str) -> Result { let interp = &mut self.interp; + if let Some(message) = &interp.install_error { + return Err(RunError { kind: RunErrorKind::Internal, message: message.clone(), line: 0, column: 0 }); + } interp.fuel = interp.limits.fuel; interp.source_units = 0; interp.throw_site = None; diff --git a/crates/js/tests/json2.rs b/crates/js/tests/json2.rs index 8692e9b30..c998e55a9 100644 --- a/crates/js/tests/json2.rs +++ b/crates/js/tests/json2.rs @@ -72,9 +72,13 @@ fn json2_polyfill_round_trips_with_the_built_in_json() { }; let source = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); let mut realm = Realm::new(); - if realm.run(NEEDS) != Ok(JsValue::Bool(true)) { - println!("json2: SKIPPED (the interpreter lacks Date, String.prototype.replace or RegExp, which json2.js uses)"); - return; + match realm.run(NEEDS) { + Ok(JsValue::Bool(true)) => {} + Ok(JsValue::Bool(false)) => { + println!("json2: SKIPPED (the interpreter lacks Date, String.prototype.replace or RegExp, which json2.js uses)"); + return; + } + other => panic!("json2: the built-in check failed: {other:?}"), } match realm.run(&format!("{BEFORE}\n{source}\n{ROUND_TRIP}")) { Ok(JsValue::String(s)) if s == "ok" => println!("json2: the polyfill and the built-in JSON agree on every sample"), diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 32585a163..193066828 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -662,7 +662,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`). The app does not run scripts yet. 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,480 of 4,556 runs pass (16 of the failing tests call `String`, `Number` or `Boolean` methods, and 22 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: