JavaScript: a small heap limit no longer leaves a realm without built-ins

Installing the built-ins is not bounded by the heap limit: Math and JSON
no longer reserve heap while they install, so a realm made with a tiny
heap_bytes gets every built-in and runs once the limit is raised. If
installing fails, the realm keeps the reason and every run ends with an
internal error instead of running against missing built-ins.

The json2.js test skips only when the built-in check returns false, and
fails on any other result. The parser's progress line says the app does
not run scripts yet.
This commit is contained in:
dotCooCoo
2026-11-08 05:35:27 -08:00
parent 0801e4373f
commit 01c89d4bc5
8 changed files with 51 additions and 12 deletions
+9 -4
View File
@@ -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}"));
}
}
-1
View File
@@ -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)?;
+1 -2
View File
@@ -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 = [
+25
View File
@@ -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
+3
View File
@@ -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<String>,
}
/// 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();
+5 -1
View File
@@ -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<JsValue, RunError> {
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;
+7 -3
View File
@@ -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"),
+1 -1
View File
@@ -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: