Merge pull request #6839 from dotCooCoo/fix/script-targetengine-error-site

Scripts: an error in a function from an earlier #targetengine run names that run's file and line
This commit is contained in:
BFlat
2026-11-12 18:18:31 +07:00
committed by GitHub
9 changed files with 335 additions and 39 deletions
File diff suppressed because one or more lines are too long
+7
View File
@@ -88,6 +88,13 @@ impl SourceMap {
let name = self.files.get(seg.file).cloned().unwrap_or_else(|| Arc::from(""));
(seg.file, name, seg.line.saturating_add(line - seg.out_line), column)
}
/// The memory the map holds, in bytes: its lists and its file names, each name counted in full
/// even when another map shares it.
pub(crate) fn bytes(&self) -> usize {
let names: usize = self.files.iter().map(|f| f.len() + 2 * size_of::<usize>()).sum();
self.files.capacity() * size_of::<Arc<str>>() + self.segments.capacity() * size_of::<Segment>() + names
}
}
/// How a preprocessor failure ends the run.
+145 -28
View File
@@ -221,12 +221,69 @@ fn current(link: &Link) -> std::result::Result<Rc<Host>, HostError> {
}
/// A named engine: its interpreter, with the globals its runs left (`None` while a run has it),
/// the link its host functions use, and its heap after its last run.
/// the link its host functions use, its heap after its last run, and the source maps of the code
/// its earlier runs compiled.
struct Engine {
name: String,
realm: Option<Realm>,
link: Link,
heap: usize,
maps: Rc<RefCell<KeptMaps>>,
}
/// A named engine taken for a run: its realm (`None` for an engine made now), its link and its
/// kept source maps.
type TakenEngine = (Option<Realm>, Link, Rc<RefCell<KeptMaps>>);
/// The most bytes of source maps ([`include::SourceMap::bytes`]) a named engine keeps from its
/// earlier runs.
pub const KEPT_MAP_BYTES: usize = 8 << 20;
/// A source map an engine kept from an earlier run: the realm's tag for the source
/// ([`Realm::error_tag`]), the map, and for the run's own script, that run's name, which stands for
/// a script given as text.
struct KeptMap {
tag: u32,
map: include::SourceMap,
script: Option<Arc<str>>,
bytes: usize,
}
/// The source maps a named engine kept from its earlier runs, by tag, oldest first, in at most
/// [`KEPT_MAP_BYTES`]: the oldest go first to make room, and a map larger than that is not kept.
#[derive(Default)]
struct KeptMaps {
maps: std::collections::VecDeque<KeptMap>,
bytes: usize,
}
impl KeptMaps {
fn keep(&mut self, tag: u32, map: include::SourceMap, script: Option<Arc<str>>) {
let bytes = map.bytes() + script.as_ref().map_or(0, |s| s.len());
let limit = kept_map_bytes();
if bytes > limit || self.maps.back().is_some_and(|m| m.tag >= tag) {
return;
}
while self.bytes + bytes > limit {
let Some(old) = self.maps.pop_front() else { break };
self.bytes -= old.bytes;
}
self.bytes += bytes;
self.maps.push_back(KeptMap { tag, map, script, bytes });
}
fn get(&self, tag: u32) -> Option<&KeptMap> {
self.maps.binary_search_by_key(&tag, |m| m.tag).ok().and_then(|i| self.maps.get(i))
}
}
/// The bytes of source maps a named engine keeps.
fn kept_map_bytes() -> usize {
#[cfg(test)]
if let Some(n) = TEST_KEPT_MAP_BYTES.with(Cell::get) {
return n;
}
KEPT_MAP_BYTES
}
/// The named engines of one thread's sessions, by their key, each in the order they were made.
@@ -665,6 +722,14 @@ struct Host {
/// The file, line and column of each error `$.evalFile` raised for a file it could not run, in
/// order: the error's mark ([`HostError::Marked`]) is its index.
eval_failures: RefCell<Vec<(Arc<str>, u32, u32)>>,
/// The realm's tag for the script ([`Realm::script_tag`]) once the run has started it, and
/// [`vectorcraft_js::UNKNOWN_SOURCE`] before.
script_tag: Cell<u32>,
/// The run's name as its report gives it, which [`Host::name`] shortens for console lines.
report_name: Arc<str>,
/// The source maps the named engine of the run kept from its earlier runs; `None` for a run
/// without one.
kept_maps: Option<Rc<RefCell<KeptMaps>>>,
/// When the run started, where the platform has a clock.
started: Option<std::time::Instant>,
/// The time since [`Host::started`] at which a `$.sleep` ends.
@@ -740,6 +805,23 @@ pub(crate) fn slowest(times: &HashMap<&'static str, (u64, u128, u128)>) -> Vec<c
}
impl Host {
/// Gives the named engine of the run the source maps of the code the run compiled: its script
/// and each file `$.evalFile` gave. A run that never started its script compiled nothing.
fn keep_maps(&self) {
let (Some(kept), tag) = (&self.kept_maps, self.script_tag.get()) else { return };
if tag == vectorcraft_js::UNKNOWN_SOURCE {
return;
}
let mut kept = kept.borrow_mut();
kept.keep(tag, std::mem::take(&mut *self.script_map.borrow_mut()), Some(self.report_name.clone()));
for (n, map) in std::mem::take(&mut *self.eval_maps.borrow_mut()).into_iter().enumerate() {
match u32::try_from(n + 1).ok().and_then(|n| tag.checked_add(n)).filter(|t| *t != vectorcraft_js::UNKNOWN_SOURCE) {
Some(t) => kept.keep(t, map, None),
None => break,
}
}
}
/// Adds a line to the run's console and, during a slice, to the session's.
fn line(&self, entry: console::Entry, cut: bool) {
let line = console::ConsoleLine { run: self.run, name: self.name.clone(), entry };
@@ -1358,34 +1440,55 @@ fn preprocessor_error(f: include::Failure) -> (RunError, Option<Arc<str>>) {
(RunError { kind, message, line: f.line, column: f.column }, file)
}
/// `e`, which the realm raised in source `tag` ([`Realm::error_source`]) with host error mark `mark`
/// ([`Realm::error_mark`]), at the line and column of the file it is in, with that file's name
/// unless it is the script's own.
fn located(host: &Host, tag: u32, mark: Option<u32>, e: &RunError) -> (RunError, Option<Arc<str>>) {
let map = |m: &include::SourceMap, own: bool| {
/// `e`, the error `realm` raised in the run of `host`, at the line and column of the file it is in,
/// with that file's name unless it is the run's own script.
fn located(host: &Host, realm: &Realm, e: &RunError) -> (RunError, Option<Arc<str>>) {
let map = |m: &include::SourceMap| {
let (index, file, line, column) = m.locate(e.line, e.column);
let message = if e.kind == RunErrorKind::Syntax { relocated(&e.message, e.line, e.column, line, column) } else { e.message.clone() };
(RunError { kind: e.kind.clone(), message, line, column }, (!own || index > 0).then_some(file))
(RunError { kind: e.kind.clone(), message, line, column }, index, file)
};
// An error `$.evalFile` raised for a file it could not run, and no script caught, is at that
// file's place.
if let Some((file, line, column)) = mark.and_then(|m| host.eval_failures.borrow().get(m as usize).cloned()) {
if let Some((file, line, column)) = realm.error_mark().and_then(|m| host.eval_failures.borrow().get(m as usize).cloned()) {
let message = if e.kind == RunErrorKind::Syntax { relocated(&e.message, e.line, e.column, line, column) } else { e.message.clone() };
return (RunError { kind: e.kind.clone(), message, line, column }, Some(file));
}
if e.line == 0 {
return (e.clone(), None);
}
// An error in code from another source, such as a function an earlier run of a named engine
// defined, has no place in this run's files, so it is given without one.
// An error in code from a source whose map is not at hand, such as code that `eval` compiled,
// is given without a place.
let unplaced = || (RunError { kind: e.kind.clone(), message: e.message.clone(), line: 0, column: 0 }, None);
match tag {
0 => map(&host.script_map.borrow(), true),
match realm.error_source() {
0 => {
let (e, index, file) = map(&host.script_map.borrow());
(e, (index > 0).then_some(file))
}
n if n != vectorcraft_js::UNKNOWN_SOURCE => match host.eval_maps.borrow().get(n as usize - 1) {
Some(m) => map(m, false),
Some(m) => {
let (e, _, file) = map(m);
(e, Some(file))
}
None => unplaced(),
},
_ => unplaced(),
// Code an earlier run of the named engine compiled, such as a function it defined, is at
// its place in that run's files: the run's script, a file it included, or a file
// `$.evalFile` gave. A script given as text is named by that run's name.
_ => {
let kept = host.kept_maps.as_ref().map(|k| k.borrow());
match kept.as_ref().and_then(|k| k.get(realm.error_tag())) {
Some(kept) => {
let (e, index, file) = map(&kept.map);
let file = match &kept.script {
Some(name) if index == 0 && file.is_empty() => name.clone(),
_ => file,
};
(e, Some(file))
}
None => unplaced(),
}
}
}
}
@@ -1438,17 +1541,17 @@ impl Session {
}
}
/// The realm of the named engine `name` for a run, with the link its host functions use: the
/// realm its last run left, or `None` for an engine made now. → the kind and text of the
/// failure when the engine cannot be had.
fn take_engine(&mut self, name: &str) -> std::result::Result<(Option<Realm>, Link), (include::FailKind, String)> {
/// The realm of the named engine `name` for a run, with the link its host functions use and the
/// source maps it kept: the realm its last run left, or `None` for an engine made now. → the
/// kind and text of the failure when the engine cannot be had.
fn take_engine(&mut self, name: &str) -> std::result::Result<TakenEngine, (include::FailKind, String)> {
if cfg!(target_arch = "wasm32") {
return Err((include::FailKind::Error, NO_ENGINES_ON_WEB.to_string()));
}
with_engines(&self.script_engines, |engines| {
if let Some(e) = engines.iter_mut().find(|e| e.name == name) {
let realm = e.realm.take().ok_or((include::FailKind::Error, ENGINE_BUSY.to_string()))?;
return Ok((Some(realm), e.link.clone()));
return Ok((Some(realm), e.link.clone(), e.maps.clone()));
}
if engines.len() >= MAX_ENGINES {
return Err((
@@ -1457,15 +1560,20 @@ impl Session {
));
}
let link: Link = Rc::new(RefCell::new(None));
engines.push(Engine { name: name.to_string(), realm: None, link: link.clone(), heap: 0 });
Ok((None, link))
let maps = Rc::new(RefCell::new(KeptMaps::default()));
engines.push(Engine { name: name.to_string(), realm: None, link: link.clone(), heap: 0, maps: maps.clone() });
Ok((None, link, maps))
})
}
/// The end of a run in engine `engine` (`None` for a realm that is not kept): the link no longer
/// holds the run, and the engine keeps `realm`, or is dropped without one.
/// holds the run, and the engine keeps `realm`, with the source maps of the code the run
/// compiled, or is dropped without one.
fn return_engine(&self, engine: Option<&str>, link: &Link, realm: Option<Realm>) {
let host = link.borrow_mut().take();
if let (Some(h), Some(_)) = (&host, &realm) {
h.keep_maps();
}
drop(host);
let Some(name) = engine else { return };
let mut realm = realm;
@@ -1668,9 +1776,9 @@ impl Session {
};
let heap_bytes = if kept.is_some() { engine_heap() } else { HEAP_BYTES };
let limits = Limits { fuel: request.fuel, heap_bytes, ..Limits::default() };
let (engine, realm, link) = match kept {
None => (None, None, Rc::new(RefCell::new(None))),
Some((engine, (realm, link))) => (Some(engine), realm, link),
let (engine, realm, link, kept_maps) = match kept {
None => (None, None, Rc::new(RefCell::new(None)), None),
Some((engine, (realm, link, maps))) => (Some(engine), realm, link, Some(maps)),
};
// A realm made here gets the script extensions, `$` and the dialogs; a kept one has them
// from its first run, and they act for the run that calls them.
@@ -1716,6 +1824,9 @@ impl Session {
script_map: RefCell::default(),
eval_maps: RefCell::default(),
eval_failures: RefCell::default(),
script_tag: Cell::new(vectorcraft_js::UNKNOWN_SOURCE),
report_name: Arc::from(name.as_str()),
kept_maps,
started: clock(),
wake_at: Cell::new(None),
can_wait: realm.wait_flag(),
@@ -1752,8 +1863,12 @@ impl Session {
.and_then(|()| if fresh { realm.define_host_object("$", dollar_object(&link, info, &localization)) } else { Ok(()) })
.and_then(|()| if fresh { dialogs::install(&mut realm, &link) } else { Ok(()) })
.and_then(|()| if fresh { files::install(&mut realm, &link) } else { realm.reset_folder_current() })
.and_then(|()| realm.start(&expanded.source))
.map_err(|e| located(&host, realm.error_source(), realm.error_mark(), &e))
.and_then(|()| {
let r = realm.start(&expanded.source);
host.script_tag.set(realm.script_tag());
r
})
.map_err(|e| located(&host, &realm, &e))
}
};
if let Err((e, file)) = started {
@@ -1832,7 +1947,7 @@ impl Session {
fn script_failed(&mut self, job: Job, e: &RunError) -> Result<Value> {
job.host.dialogs.stopped();
let fuel_left = job.realm.fuel_left();
let (e, file) = located(&job.host, job.realm.error_source(), job.realm.error_mark(), e);
let (e, file) = located(&job.host, &job.realm, e);
let mut report = failure_report(&job.name, &e, file.as_deref(), &job.host, fuel_left, job.realm.limits().fuel.saturating_sub(fuel_left));
close_run(self, &job.host, &mut report, ending(&e.kind));
self.end_job(job, e.kind == RunErrorKind::Internal);
@@ -2127,6 +2242,8 @@ thread_local! {
pub(crate) static TEST_GROWTH: Cell<Option<u64>> = const { Cell::new(None) };
/// Tests: the heap of a named engine in place of [`ENGINE_HEAP_BYTES`].
pub(crate) static TEST_ENGINE_HEAP: Cell<Option<usize>> = const { Cell::new(None) };
/// Tests: the bytes of source maps a named engine keeps, in place of [`KEPT_MAP_BYTES`].
pub(crate) static TEST_KEPT_MAP_BYTES: Cell<Option<usize>> = const { Cell::new(None) };
/// Tests: the `JSINCLUDE` folders in place of the environment's.
pub(crate) static TEST_JSINCLUDE: RefCell<Option<Vec<std::path::PathBuf>>> = const { RefCell::new(None) };
/// Tests: the time since a run started, in place of its clock's.
+108 -4
View File
@@ -6,7 +6,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use crate::script::{PANIC_IN_SLICE, TEST_ENGINE_HEAP, engines_on_this_thread, releases_kept_for};
use crate::script::{PANIC_IN_SLICE, TEST_ENGINE_HEAP, TEST_KEPT_MAP_BYTES, engines_on_this_thread, releases_kept_for};
use crate::*;
fn run(s: &mut Session, source: &str) -> Result<Value> {
@@ -119,11 +119,115 @@ fn only_the_script_itself_names_its_engine() {
assert_eq!(report["line"], 2, "{report}");
assert_eq!(names(&mut s), ["e"]);
let _ = std::fs::remove_dir_all(base);
// An error in a function an earlier run defined has no place in the run that calls it.
// An error in a function an earlier run defined is at that run's line; a script given as text
// is named by its run's name.
run(&mut s, "#targetengine e\n\n\n\nfunction bad() { throw new Error('kept'); }").unwrap();
assert!(run(&mut s, "#targetengine e\nbad();").is_err());
let e = run(&mut s, "#targetengine e\nbad();").unwrap_err().to_string();
let report = s.script_report().unwrap().clone();
assert!(report["line"].as_u64().is_none_or(|l| l == 0), "{report}");
assert_eq!((report["name"].as_str(), report["file"].as_str(), report["line"].as_u64()), (Some("t.js"), Some("t.js"), Some(5)), "{report}");
let column = report["column"].as_u64().unwrap();
assert_eq!(error_lines(&report), [format!("t.js: t.js:5:{column}: Error: kept")]);
assert!(e.contains(&format!("line 5, column {column} of t.js")), "{e}");
}
/// The console error lines of a run's report.
fn error_lines(report: &Value) -> Vec<String> {
let lines = report["console"]["lines"].as_array().unwrap();
lines.iter().filter_map(Value::as_str).filter(|l| l.contains("Error: ")).map(str::to_string).collect()
}
/// The file, line and column of the last run's error.
fn place(s: &Session) -> (Option<String>, u64, u64) {
let r = s.script_report().unwrap();
(r["file"].as_str().map(str::to_string), r["line"].as_u64().unwrap(), r["column"].as_u64().unwrap())
}
/// An error in code an earlier run of a named engine compiled is at the place of that code: in that
/// run's script, a file it included or a file `$.evalFile` gave, while the report and the console
/// line keep the calling run's name.
#[test]
fn an_error_in_code_an_earlier_run_kept_is_at_that_runs_file_and_line() {
let base: PathBuf = std::env::temp_dir().join(format!("vc-script-engines-places-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
std::fs::write(base.join("inc.jsxinc"), "\nfunction fromInclude() { throw new Error('inc'); }").unwrap();
std::fs::write(base.join("ev.jsxinc"), "\n\n\nfunction fromEval() { throw new Error('ev'); }").unwrap();
let def = base.join("def.jsx");
let text = "#targetengine e\n#include \"inc.jsxinc\"\n$.evalFile('ev.jsxinc');\nfunction fromMain() {\n throw new Error('main');\n}\n\
function spin() {\n while (true) {}\n}\nfunction badEval() {\n eval('var = 1');\n}";
std::fs::write(&def, text).unwrap();
let mut s = Session::new();
s.execute("script.run", &json!({"path": def.to_string_lossy()})).unwrap();
let call = |s: &mut Session, code: &str| {
let e = s.execute("script.run", &json!({"source": format!("#targetengine e\n\n{code}"), "name": "caller.js"}));
assert!(e.is_err(), "{code}");
};
let shown = def.to_string_lossy().to_string();
let ends = |p: &(Option<String>, u64, u64), file: &str, line: u64| p.0.as_deref().is_some_and(|f| f.ends_with(file)) && p.1 == line && p.2 > 0;
call(&mut s, "fromMain();");
let p = place(&s);
assert!(ends(&p, "def.jsx", 5), "{p:?}");
let report = s.script_report().unwrap().clone();
assert_eq!(report["name"], "caller.js");
assert_eq!(error_lines(&report), [format!("caller.js: {}:5:{}: Error: main", p.0.as_deref().unwrap(), p.2)]);
call(&mut s, "fromInclude();");
assert!(ends(&place(&s), "inc.jsxinc", 2), "{:?}", place(&s));
call(&mut s, "fromEval();");
assert!(ends(&place(&s), "ev.jsxinc", 4), "{:?}", place(&s));
call(&mut s, "badEval();");
assert!(ends(&place(&s), "def.jsx", 11), "a syntax error eval raises is at its call: {:?}", place(&s));
let e = s.execute("script.run", &json!({"source": "#targetengine e\nspin();", "name": "caller.js", "maxFuel": 100_000}));
assert!(e.is_err());
assert_eq!(s.script_report().unwrap()["kind"], "limit");
assert!(ends(&place(&s), "def.jsx", 8), "the fuel ran out in spin: {:?}", place(&s));
// A caught error thrown again is where that throw is, in the calling run.
call(&mut s, "try { fromMain(); } catch (x) {\n throw x;\n}");
assert_eq!(place(&s), (None, 4, 3));
// A run that fails before it starts compiles nothing, and the kept places stay.
assert!(run(&mut s, "#targetengine e\n#include \"missing.jsxinc\"").is_err());
call(&mut s, "fromMain();");
assert!(ends(&place(&s), "def.jsx", 5), "{:?}", place(&s));
assert!(place(&s).0.is_some_and(|f| f == shown));
// A dropped engine takes its places with it: the new one has its own.
s.execute("script.engine.drop", &json!({"name": "e"})).unwrap();
run(&mut s, "#targetengine e\nfunction fromMain() { throw new Error('new'); }").unwrap();
call(&mut s, "fromMain();");
assert_eq!(place(&s).0.as_deref(), Some("t.js"));
assert_eq!(place(&s).1, 2);
// The name stands for the earlier script as that run's report gave it, not as console lines
// show it.
s.execute("script.run", &json!({"source": "#targetengine e\nfunction named() { throw 1; }", "name": "a\u{1}b.js"})).unwrap();
call(&mut s, "named();");
assert_eq!(place(&s).0.as_deref(), Some("a\u{1}b.js"));
let _ = std::fs::remove_dir_all(base);
}
/// A named engine keeps the places of its earlier runs' code within its budget, the oldest going
/// first, and the code still runs once its place is gone.
#[test]
fn an_engine_keeps_the_newest_places_within_its_budget() {
let mut s = Session::new();
run(&mut s, "#targetengine e\nfunction a() { throw new Error('a'); }").unwrap();
TEST_KEPT_MAP_BYTES.with(|b| b.set(Some(300)));
run(&mut s, "#targetengine e\n\nfunction b() { throw new Error('b'); }").unwrap();
for _ in 0..20 {
run(&mut s, "#targetengine e\nvar x = 1;").unwrap();
}
run(&mut s, "#targetengine e\n\n\nfunction c() { throw new Error('c'); }").unwrap();
// Each run's own map takes the room of the one before, so c is called first.
assert!(run(&mut s, "#targetengine e\nc();").is_err());
let c_place = place(&s);
let a = run(&mut s, "#targetengine e\na();").map(drop);
let a_place = place(&s);
TEST_KEPT_MAP_BYTES.with(|b| b.set(Some(1)));
run(&mut s, "#targetengine e\n\n\n\nfunction d() { throw new Error('d'); }").unwrap();
assert!(run(&mut s, "#targetengine e\nd();").is_err());
let d_place = place(&s);
TEST_KEPT_MAP_BYTES.with(|b| b.set(None));
assert!(a.is_err());
assert_eq!((a_place.0, a_place.1, a_place.2), (None, 0, 0), "a's place went to make room");
assert_eq!((c_place.0.as_deref(), c_place.1), (Some("t.js"), 4));
assert_eq!((d_place.0, d_place.1, d_place.2), (None, 0, 0), "a map larger than the budget is not kept");
}
/// A `$.evalFile` an earlier run kept reads files the way the run that calls it does: relative to
+1 -1
View File
@@ -395,7 +395,7 @@ impl Interp {
Err(e) => return Err(self.host_error(e)?),
};
let writes = strict_writes().unwrap_or_else(|| self.writes_mode());
self.evaluated = self.evaluated.saturating_add(1);
self.evaluated = crate::realm::next_tag(self.evaluated);
let tag = std::mem::replace(&mut self.source_tag, self.evaluated);
// UTF-8 takes at most 3 bytes for each UTF-16 code unit, so the fuel for a third of the
// bytes is taken before the source is encoded, and the rest after. A source within the
+31
View File
@@ -638,6 +638,37 @@ fn error_sources_count_from_the_last_script_and_code_from_an_earlier_one_is_unkn
assert_eq!(r.error_source(), 0, "a syntax error is in the script");
}
#[test]
fn error_tags_stay_the_same_from_run_to_run_and_are_never_shared() {
let mut r = realm(10_000_000);
let source: HostFunction = Rc::new(|_| Ok(Data::String("function inEval() { throw new Error('in eval'); }".into())));
let load = HostItem::Evaluator { source, end: Rc::new(|| {}), strict_writes: Rc::new(|| None) };
r.define_host_object("host", vec![("load".into(), load)]).unwrap();
assert_eq!(r.script_tag(), UNKNOWN_SOURCE, "no script yet");
r.run("function late() { throw new Error('late'); }\nhost.load(); host.load();").unwrap();
let first = r.script_tag();
assert_ne!(first, UNKNOWN_SOURCE);
assert!(r.run("\nlate()").is_err());
let second = r.script_tag();
assert!(second > first + 2, "the evaluator's two sources took the tags after the first script");
assert_eq!((r.error_source(), r.error_tag()), (UNKNOWN_SOURCE, first), "late keeps its script's tag");
assert!(r.run("inEval()").is_err());
assert_eq!(r.error_tag(), first + 2, "the second source the evaluator gave in the first run");
assert!(r.run("\nthrow 1").is_err());
assert_eq!((r.error_source(), r.error_tag()), (0, r.script_tag()));
// Once the tags are used up, a source gets no tag rather than one an earlier source has.
r.set_last_tag(UNKNOWN_SOURCE - 3);
r.run("function nearEnd() { throw 1; }\nhost.load(); host.load(); host.load();").unwrap();
assert_eq!(r.script_tag(), UNKNOWN_SOURCE - 2);
assert!(r.run("nearEnd()").is_err());
assert_eq!(r.script_tag(), UNKNOWN_SOURCE, "a script after the last tag");
assert_eq!(r.error_tag(), UNKNOWN_SOURCE - 2);
assert!(r.run("inEval()").is_err());
assert_eq!(r.error_tag(), UNKNOWN_SOURCE, "the third source came after the last tag");
assert!(r.run("\nthrow 1").is_err());
assert_eq!((r.error_source(), r.error_tag()), (UNKNOWN_SOURCE, UNKNOWN_SOURCE));
}
#[test]
fn an_evaluators_source_takes_fuel_before_it_is_parsed() {
// The evaluator gives 131,072 spaces and a number: far more code units than the fuel left.
+34 -3
View File
@@ -63,6 +63,12 @@ pub enum LimitKind {
/// evaluator's: code that `eval` or `Function` compiled, or an error with no place.
pub const UNKNOWN_SOURCE: u32 = u32::MAX;
/// The source tag after `tag`. Once the tags are used up, every later source gets
/// [`UNKNOWN_SOURCE`], so no two sources share a tag.
pub(crate) fn next_tag(tag: u32) -> u32 {
if tag >= UNKNOWN_SOURCE - 1 { UNKNOWN_SOURCE } else { tag + 1 }
}
/// The fuel [`Realm::advance`] lets a slice spend, past the slice it was asked for, in a nested run
/// it cannot pause.
pub const MAX_UNBROKEN_FUEL: u64 = 200_000_000;
@@ -151,7 +157,9 @@ pub struct Realm {
unbroken_fuel: u64,
/// The source tag of the script the last [`Realm::run`] or [`Realm::start`] compiled. Each
/// script gets a new one, and the sources an evaluator gives during its run get the ones after
/// it, so code that an earlier script defined keeps a tag of its own.
/// it, so code that an earlier script defined keeps a tag of its own. [`UNKNOWN_SOURCE`]
/// before the first script, for a script the realm did not compile, and once the tags are used
/// up.
script_tag: u32,
}
@@ -170,7 +178,7 @@ impl Realm {
/// `options`; [`Options::max_source_units`] caps the source parsed in one run, all of it
/// counted together.
pub fn with_options(options: Options, limits: Limits) -> Self {
Self { interp: Interp::new(options, limits), unbroken_fuel: MAX_UNBROKEN_FUEL, script_tag: 0 }
Self { interp: Interp::new(options, limits), unbroken_fuel: MAX_UNBROKEN_FUEL, script_tag: UNKNOWN_SOURCE }
}
/// Sets the fuel [`Realm::advance`] lets a nested run spend past its slice (by default
@@ -226,6 +234,7 @@ impl Realm {
/// Compiles `source` and readies the realm for a run.
fn compile(&mut self, source: &str) -> Result<std::rc::Rc<crate::bytecode::Code>, RunError> {
self.script_tag = UNKNOWN_SOURCE;
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 });
@@ -244,7 +253,7 @@ impl Realm {
interp.finally_sites.clear();
interp.error_mark = None;
let strict = interp.options.strict;
interp.evaluated = interp.evaluated.saturating_add(1).min(UNKNOWN_SOURCE - 1);
interp.evaluated = next_tag(interp.evaluated);
self.script_tag = interp.evaluated;
interp.source_tag = self.script_tag;
let code = interp.encode_source(source).and_then(|units| interp.compile_source(&units, strict, CodeKind::Global));
@@ -397,6 +406,28 @@ impl Realm {
}
}
/// The source tag of the code the last error's `line` and `column` are in, which stays the same
/// from run to run: each script given to [`Realm::run`] or [`Realm::start`] and each source a
/// [`crate::HostItem::Evaluator`] gives gets the next tag, so a function an earlier script
/// defined has its script's tag. [`UNKNOWN_SOURCE`] for code that `eval` or `Function`
/// compiled, an error with no place, and sources given once the tags are used up.
pub fn error_tag(&self) -> u32 {
self.interp.error_tag
}
/// The source tag ([`Realm::error_tag`]) of the script the last [`Realm::run`] or
/// [`Realm::start`] compiled; the `n`th source an evaluator gave during that run has this tag
/// plus `n`. [`UNKNOWN_SOURCE`] when that call compiled nothing.
pub fn script_tag(&self) -> u32 {
self.script_tag
}
/// Tests: makes `tag` the last source tag given out.
#[cfg(test)]
pub(crate) fn set_last_tag(&mut self, tag: u32) {
self.interp.evaluated = tag;
}
/// The mark of the host error ([`crate::HostError::Marked`]) the last run ended with, when no
/// `catch` clause took that error's exception on its way out; `None` otherwise.
pub fn error_mark(&self) -> Option<u32> {
+7 -2
View File
@@ -1873,8 +1873,13 @@ no engines, and the engines it left are dropped when their own thread next uses
and the other eight are kept. Each named engine has a heap of 64 MiB, which holds what it keeps between runs; a run
that goes past it ends with the heap limit, and the engine stays usable.
- The documents go back after an uncaught error, a limit or `script.stop` as they do for any run, and the engine keeps
the globals the run set before it ended. An internal error drops the engine. An error in a function an earlier run
defined is reported without a line and column.
the globals the run set before it ended. An internal error drops the engine.
- An error in code an earlier run compiled, such as a function it defined, is reported at that code's line and column
in the earlier run's script, a file it included or a file its `$.evalFile` ran, and the report's `file` names that
file. For an earlier script given as `source`, `file` is that run's name. The report's `name` and the console line's
name stay the calling run's, as in `caller.js: /path/lib.jsx:12:5: Error: kept`. Each engine keeps these places
for 8 MiB of source maps, the oldest going first; an error in code whose place is gone, or in code that `eval` or
`Function` compiled, is reported without a line and column, and the code still runs.
- `vc`, `$`, `alert`, `confirm` and `prompt` act for the run that calls them, including a copy an earlier run kept in a
global. `vc.args` is the calling run's `args`, through a `vc` object an earlier run kept too. An engine
holds no document: `vc.document` is a copy, read when it is called. What a run sets on `$` (`$.locale`,
+1
View File
@@ -756,6 +756,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Scripts: an error a `finally` block passes on is still reported where it was thrown when the block's allocations collect garbage under a small heap limit, and a `finally` block that `return`, `break` or `continue` leaves drops the saved location of its error as it leaves, so a collection no longer counts that code as live. VectorCraft's own rule. `crates/js/src/host_tests.rs`.
- Scripts: a run reads its preprocessor lines before any of it runs: `#include` and `//@include` put another file in place of the line, looked for beside the including file, in the `#includepath` folders and in `JSINCLUDE`; `#script` names the run; `#target` and `#strict off` are taken, `#strict on` turns on strict write checks (below), `#targetengine` names the engine (below), and `#engine` is refused. `$.evalFile` runs a file with the same globals and returns its last value, and `$.fileName` and `$.includePath` give the file being run and the include folders. A script run from a file reads these files only in its folder and below, without `..` names or links that lead out, and an error in one is reported at its own file, line and column ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/), [the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/); `crates/engine/src/tests/script_include.rs`).
- Scripts: `#targetengine name` (and `//@targetengine name`) runs the script in a named engine that lasts for the session, and a later script that names it sees the globals and functions earlier ones left ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/)); `$.engineName` gives the name ([the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: a script without the line and `#targetengine main` get a new engine each run, the line comes once in the script itself before its code, a session keeps 8 named engines with 64 MiB of heap each, an error or a stop keeps the globals the run set while the documents go back, kept host functions act for the run that calls them, `script.engines` lists the engines and `script.engine.drop` drops one, and the web build has none. `crates/engine/src/tests/script_engines.rs`.
- Scripts: a runtime error is shown at the line that was running ([the help page](https://extendscript.docsforadobe.dev/extendscript-toolkit/debugging-in-the-toolkit/)). VectorCraft's own rules: an error in code an earlier run of a named engine compiled, such as a function it defined, is reported at that code's file, line and column (the earlier run's script, a file it included or a file its `$.evalFile` ran), while the report and its console line keep the calling run's name; a script given as text is named by its run's name, and each engine keeps these places for 8 MiB of source maps, the oldest going first. `crates/engine/src/tests/script_engines.rs`.
- Scripts: `File` and `Folder` ([File object](https://extendscript.docsforadobe.dev/file-system-access/file-object/), [Folder object](https://extendscript.docsforadobe.dev/file-system-access/folder-object/)) name files and folders by platform or URI path, with `name`, `fsName`, `absoluteURI`, `relativeURI`, `parent`, `Folder.current` and `encode`/`decode`, and read them: `exists`, `length`, the dates, `getFiles` with a mask or a function, and `open('r')` with `read`, `readln`, `readch`, `seek`, `tell` and `close`, decoding by byte order mark, `encoding`, UTF-8 or CP1252. VectorCraft's own rules: a script reads only in its own folder and below (and inside the automation read roots when they are set); any other path and a network or device path are refused before anything is looked up, and a link that leads out is refused as well (`false` or `null`, with `error` set); a file is read whole up to 8 MiB and a run reads up to 32 MiB; writing, the file dialogs and `execute` are not available yet. `crates/engine/src/tests/script_io.rs`.
- Scripts: `#strict on` (and `//@strict on`) runs the script with the checks the [`$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/) help page gives for `$.strict`: an assignment to a read-only property throws a `TypeError`, and so does a new property on an object that takes none. VectorCraft's own rules: a getter-only property counts as read-only, the objects that take no new properties are the non-extensible ones (`Object.preventExtensions`, `seal`, `freeze`, and the frozen `vc` and `$` objects), the last `#strict` line of the script and its includes sets the whole run, a file `$.evalFile` runs keeps its own line's checks for its code, and the rest of ES5 strict mode stays with `'use strict'`. Without the line, runs are unchanged. `crates/engine/src/tests/script_include.rs` (`strict_on_*`, `strict_mode_in_eval_file_is_scoped`), `crates/js/src/interp_tests.rs` (`strict_writes_are_separate_from_es5_strict`).
- Scripts folder: File › Scripts lists the `.js` and `.jsx` files of the `Scripts` folder next to the preferences, each named after its file without the extension, with each subfolder as a submenu, above Check Script… and Other Script… (https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/install-and-run-scripts.html). VectorCraft's own rules: the folder is read again when the window becomes active and when the File menu opens (in the macOS menu bar, when any of its menus opens, and the new listing shows the next time the menu opens), links are not followed, and one read stops at 2,048 entries, 512 scripts, 128 subfolders, 4 levels or 250 ms. `script.listInstalled` returns the listing. `crates/engine/src/tests/script_folder.rs`, `crates/ui-egui/src/tests/scripts_menu.rs`.