diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 53f6e18f75..e82ab1e275 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -822,6 +822,34 @@ impl Host { } } + /// The file and line that line `line` of the source with tag `tag` came from, as `$.line` and + /// `$.stack` give them: the run's script, a file it included or a file `$.evalFile` gave, in + /// this run or in an earlier run of its named engine, with the run's name standing for a script + /// given as text. Code that `eval` or `Function` compiled has no map and keeps its own line, + /// named `[eval]`; code whose map is gone, such as one the named engine let go to stay within + /// [`KEPT_MAP_BYTES`], is at line 0 of `[unknown]`. + fn source_place(&self, tag: u32, line: u32) -> (String, u32) { + if tag == vectorcraft_js::UNKNOWN_SOURCE { + return ("[eval]".to_string(), line); + } + let named = |map: &include::SourceMap, script: Option<&Arc>| { + let (index, file, line, _) = map.locate(line, 1); + match script { + Some(name) if index == 0 && file.is_empty() => (name.to_string(), line), + _ => (file.to_string(), line), + } + }; + let script = self.script_tag.get(); + let placed = match tag.checked_sub(script).filter(|_| script != vectorcraft_js::UNKNOWN_SOURCE) { + Some(0) => self.script_map.try_borrow().ok().map(|m| named(&m, Some(&self.report_name))), + Some(n) => self.eval_maps.try_borrow().ok().and_then(|maps| maps.get(n as usize - 1).map(|m| named(m, None))), + None => { + self.kept_maps.as_ref().and_then(|k| k.try_borrow().ok()).and_then(|k| k.get(tag).map(|kept| named(&kept.map, kept.script.as_ref()))) + } + }; + placed.unwrap_or_else(|| ("[unknown]".to_string(), 0)) + } + /// 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 }; @@ -1858,6 +1886,13 @@ impl Session { Ok(expanded) => { *host.script_map.borrow_mut() = expanded.map; realm.set_strict_writes(expanded.strict == Some(true)); + if fresh { + let l = link.clone(); + realm.set_source_locator(Rc::new(move |tag, line| match current(&l) { + Ok(h) => h.source_place(tag, line), + Err(_) => ("[unknown]".to_string(), 0), + })); + } (if fresh { realm.add_script_extensions() } else { Ok(()) }) .and_then(|()| if fresh { realm.define_host_object("vc", vc_object(&link)) } else { Ok(()) }) .and_then(|()| if fresh { realm.define_host_object("$", dollar_object(&link, info, &localization)) } else { Ok(()) }) diff --git a/crates/engine/src/tests/script_dollar.rs b/crates/engine/src/tests/script_dollar.rs index 57ff9b15cd..849d5b9080 100644 --- a/crates/engine/src/tests/script_dollar.rs +++ b/crates/engine/src/tests/script_dollar.rs @@ -63,7 +63,7 @@ fn each_member_has_its_value() { ("$.memCache", json!(0)), ("$.os", json!(os)), ("$.screens.length", json!(0)), - ("$.stack", json!("[global]:1\n")), + ("$.stack", json!("[global]@t.js:1\n")), ("$.strict", json!(false)), ("$.version", json!(version)), ("typeof $.getenv + typeof $.setenv + typeof $.sleep", json!("functionfunctionfunction")), @@ -165,7 +165,7 @@ fn getenv_reads_only_its_names_and_setenv_changes_only_the_run() { fn error_line_and_stack_follow_the_running_code() { let mut s = Session::new(); let source = "try { null.x; } catch (e) {}\nfunction where() {\n return [$.line, $.stack, $.error instanceof TypeError];\n}\nvc.log(where());"; - assert_eq!(logged(&mut s, source)[0], json!([3, "[global]:5\nwhere:3\n", true])); + assert_eq!(logged(&mut s, source)[0], json!([3, "[global]@t.js:5\nwhere@t.js:3\n", true])); assert_eq!(value(&mut s, "$.error"), json!(null), "each run starts without one"); } diff --git a/crates/engine/src/tests/script_engines.rs b/crates/engine/src/tests/script_engines.rs index 7e768c5875..5edec3b1c9 100644 --- a/crates/engine/src/tests/script_engines.rs +++ b/crates/engine/src/tests/script_engines.rs @@ -230,6 +230,44 @@ fn an_engine_keeps_the_newest_places_within_its_budget() { assert_eq!((d_place.0, d_place.1, d_place.2), (None, 0, 0), "a map larger than the budget is not kept"); } +/// `$.line` and `$.stack` in code an earlier run of a named engine compiled give the line of that +/// run's file: its script, or a file it included; a script given as text is named by that run's +/// name. Code `eval` and `Function` compiled keeps its own line, in `[eval]`, and code whose place +/// the engine did not keep is at line 0 of `[unknown]`. +#[test] +fn the_line_and_the_stack_of_code_an_earlier_run_kept_are_at_that_runs_files() { + use super::script_include::short_stack; + let base: PathBuf = std::env::temp_dir().join(format!("vc-script-engines-line-stack-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir_all(&base).unwrap(); + std::fs::write(base.join("inc.jsxinc"), "\n\nfunction fromInclude() {\n return [$.line, $.stack];\n}").unwrap(); + let def = base.join("def.jsx"); + std::fs::write(&def, "#targetengine e\nvar pad = 1;\n#include \"inc.jsxinc\"\nfunction fromMain() {\n return fromInclude();\n}").unwrap(); + let mut s = Session::new(); + s.execute("script.run", &json!({"path": def.to_string_lossy()})).unwrap(); + run(&mut s, "#targetengine e\n\nfunction fromText() {\n return $.stack;\n}").unwrap(); + let call = |s: &mut Session, code: &str| { + let r = s.execute("script.run", &json!({"source": format!("#targetengine e\n\n{code}"), "name": "caller.js"})); + r.unwrap_or_else(|e| panic!("{code}: {e}"))["log"][0].clone() + }; + let r = call(&mut s, "vc.log(fromMain());"); + assert_eq!(r[0], 4, "{r}"); + assert_eq!(short_stack(&r[1]), "[global]@caller.js:3\nfromMain@def.jsx:5\nfromInclude@inc.jsxinc:4\n"); + assert_eq!(short_stack(&call(&mut s, "vc.log(fromText());")), "[global]@caller.js:3\nfromText@t.js:4\n"); + assert_eq!(call(&mut s, "vc.log(eval('\\n\\n$.line'));"), 3); + assert_eq!(call(&mut s, "vc.log(eval('\\n$.stack'));"), "[global]@caller.js:3\n[global]@[eval]:2\n"); + let r = call(&mut s, "vc.log(new Function('\\nreturn $.stack;')());"); + assert_eq!(r, "[global]@caller.js:3\n[anonymous]@[eval]:4\n", "the line of the function source `Function` compiles"); + // A map larger than the budget is not kept. + TEST_KEPT_MAP_BYTES.with(|b| b.set(Some(1))); + let lost = run(&mut s, "#targetengine e\n\n\nfunction lost() {\n return [$.line, $.stack];\n}"); + let r = lost.map(|_| call(&mut s, "vc.log(lost());")); + TEST_KEPT_MAP_BYTES.with(|b| b.set(None)); + let r = r.unwrap(); + assert_eq!(r, json!([0, "[global]@caller.js:3\nlost@[unknown]:0\n"])); + let _ = std::fs::remove_dir_all(base); +} + /// A run of a named engine whose script does not compile defines no code, so it keeps no place /// and pushes out none of the places kept before. An included file without a preprocessor line is /// first parsed with the script it joins, so its syntax error ends the run as it starts. diff --git a/crates/engine/src/tests/script_include.rs b/crates/engine/src/tests/script_include.rs index e5acc536cc..1d487e44bf 100644 --- a/crates/engine/src/tests/script_include.rs +++ b/crates/engine/src/tests/script_include.rs @@ -454,6 +454,49 @@ fn script_included_error_has_local_file_and_line() { let _ = std::fs::remove_dir_all(base); } +/// `$.stack`'s text with each frame's file cut to its name: `function@name:line` a line. +pub(crate) fn short_stack(stack: &Value) -> String { + let stack = stack.as_str().unwrap_or_else(|| panic!("a stack: {stack}")); + stack + .lines() + .map(|frame| match (frame.split_once('@'), frame.rsplit_once(':')) { + (Some((name, _)), Some((place, line))) => { + let file = place.split_once('@').map_or(place, |(_, f)| f).rsplit(['/', '\\']).next().unwrap_or_default(); + format!("{name}@{file}:{line}\n") + } + _ => format!("{frame}\n"), + }) + .collect() +} + +/// `$.line` and `$.stack` give the line of the file the running code is in: the script, a file it +/// included (and a file that one included) or a file `$.evalFile` gave and the files it included, +/// whatever lines come before it in the run's expanded source. +#[test] +fn script_line_and_stack_are_at_the_line_of_each_file() { + let base = folder("line-stack"); + write( + &base.join("main/lib.jsxinc"), + "\nfunction libWhere() {\n return [$.line, $.stack];\n}\nvar libLine = $.line;\n#include \"deep.jsxinc\"\n", + ); + write(&base.join("main/deep.jsxinc"), "\n\nvar deepLine = $.line;\nfunction deepStack() {\n return $.stack;\n}"); + write(&base.join("main/sub/ev.jsx"), "#include \"inc.jsxinc\"\nfunction evWhere() {\n return incWhere();\n}\nevWhere();"); + write(&base.join("main/sub/inc.jsxinc"), "\n\nfunction incWhere() {\n return [$.line, $.stack];\n}"); + let text = "var a = 1;\nvar b = 2;\n#include \"lib.jsxinc\"\nvar mainLine = $.line;\nvc.log([libLine, deepLine, mainLine]);\n\ + vc.log(libWhere());\nvc.log(deepStack());\nvc.log($.evalFile('sub/ev.jsx'));"; + let log = run(&base, text).unwrap()["log"].clone(); + assert_eq!(log[0], json!([5, 3, 4]), "{log}"); + assert_eq!(log[1][0], 3, "{log}"); + assert_eq!(short_stack(&log[1][1]), "[global]@main.jsx:6\nlibWhere@lib.jsxinc:3\n"); + assert_eq!(short_stack(&log[2]), "[global]@main.jsx:7\ndeepStack@deep.jsxinc:5\n"); + assert_eq!(log[3][0], 4, "{log}"); + assert_eq!(short_stack(&log[3][1]), "[global]@main.jsx:8\n[global]@ev.jsx:5\nevWhere@ev.jsx:3\nincWhere@inc.jsxinc:4\n"); + // The file a frame names is the one `$.fileName` gives while that file runs. + let log = run(&base, "vc.log($.stack.slice(9, -3) === $.fileName);").unwrap()["log"].clone(); + assert_eq!(log, json!([true])); + let _ = std::fs::remove_dir_all(base); +} + /// Pumps the run in the app until it ends or waits at a dialog (`None`). fn pump(s: &mut Session) -> Option> { for _ in 0..10_000 { diff --git a/crates/js/src/builtins/extensions.rs b/crates/js/src/builtins/extensions.rs index 4ef9b82105..94ee226d57 100644 --- a/crates/js/src/builtins/extensions.rs +++ b/crates/js/src/builtins/extensions.rs @@ -388,18 +388,33 @@ fn last_error(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult JsResult { - let line = interp.script_frames().last().map_or(0, |(code, pc, _)| code.line_column(pc.saturating_sub(1)).0); + let line = interp.script_frames().last().map_or(0, |(code, pc, _)| place(interp.locator.as_ref(), code, pc).1); Ok(Value::Number(f64::from(line))) } +/// Where the instruction before `pc` in `code` came from: the file the realm's locator names +/// (`None` without one) and the line there, or the line in the compiled source without a locator. +fn place(locator: Option<&crate::realm::SourceLocator>, code: &Code, pc: usize) -> (Option, u32) { + let line = code.line_column(pc.saturating_sub(1)).0; + match locator { + Some(locate) => { + let (file, line) = locate(code.source.tag, line); + (Some(file), line) + } + None => (None, line), + } +} + /// The most frames `$.stack` lists, and the most characters of a function's name it shows. const STACK_FRAMES: usize = 64; const STACK_NAME_UNITS: usize = 64; -/// The script's frames, outermost first, one a line: the function's name and the line it is at. +/// The script's frames, outermost first, one a line: the function's name and the line it is at, +/// as `name:line`, or as `name@file:line` with the file the realm's locator names. fn stack(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult { let frames: Vec<(Rc, usize, bool)> = interp.script_frames().map(|(c, pc, f)| (c.clone(), pc, f)).collect(); let skip = frames.len().saturating_sub(STACK_FRAMES); + let locator = interp.locator.clone(); let mut text = String::new(); if skip > 0 { text.push_str("…\n"); @@ -407,8 +422,10 @@ fn stack(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult for (code, pc, function) in frames.iter().skip(skip) { interp.use_fuel()?; let name = if *function { function_name(code) } else { "[global]".to_string() }; - let line = code.line_column(pc.saturating_sub(1)).0; - text.push_str(&format!("{name}:{line}\n")); + match place(locator.as_ref(), code, *pc) { + (Some(file), line) => text.push_str(&format!("{name}@{file}:{line}\n")), + (None, line) => text.push_str(&format!("{name}:{line}\n")), + } } let mut out = Builder::new(); out.push_str(interp, &text)?; diff --git a/crates/js/src/extension_tests.rs b/crates/js/src/extension_tests.rs index 8336643e4d..4807856bf5 100644 --- a/crates/js/src/extension_tests.rs +++ b/crates/js/src/extension_tests.rs @@ -219,6 +219,32 @@ fn script_items_read_the_global_object_the_last_error_the_line_and_the_stack() { assert!(thrown(&mut r, "'use strict'; h.line = 1").starts_with("TypeError"), "the items cannot be assigned"); } +#[test] +fn the_line_and_the_stack_give_the_place_the_source_locator_maps_each_frame_to() { + let mut r = realm(); + let items = [("line", ScriptItem::Line), ("stack", ScriptItem::Stack)].into_iter().map(|(n, i)| (n.to_string(), HostItem::Script(i))).collect(); + r.define_host_object("h", items).unwrap(); + let asked: Rc>> = Rc::default(); + let seen = asked.clone(); + r.set_source_locator(Rc::new(move |tag, line| { + seen.borrow_mut().push((tag, line)); + let file = if tag == UNKNOWN_SOURCE { "[eval]".to_string() } else { format!("f{tag}") }; + (file, line + 100) + })); + let source = "function inner() {\n return h.stack;\n}\n\ninner()"; + let stack = text(&mut r, source); + let tag = r.script_tag(); + assert_eq!(stack, format!("[global]@f{tag}:105\ninner@f{tag}:102\n")); + assert_eq!(*asked.borrow(), [(tag, 5), (tag, 2)], "each frame's tag and its line in the compiled source"); + assert_eq!(r.run("\n\n h.line").unwrap(), JsValue::Number(103.0)); + let tag = r.script_tag(); + let stack = text(&mut r, "\neval('\\n\\nh.stack')"); + assert_eq!(stack, format!("[global]@f{}:102\n[global]@[eval]:103\n", tag + 1), "code eval compiles has no tag of its own"); + let deep = "function d(n) { return n ? d(n - 1) : h.stack; } var s = d(100).split('\\n'); s.length + ',' + s[0] + ',' + s[1]"; + let tag = r.script_tag() + 1; + assert_eq!(text(&mut r, deep), format!("66,…,d@f{tag}:101"), "the frames past 64 are still left out"); +} + #[test] fn a_host_property_reads_its_getter_and_gives_assignments_to_its_setter() { let mut r = Realm::new(); diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 22a365b615..edc5282fe5 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -232,6 +232,8 @@ pub(crate) struct Interp { pub(crate) evaluated: u32, /// The tag of the source the last run's error was raised in. pub(crate) error_tag: u32, + /// Where `$.line` and `$.stack` find the file and line of a compiled line. + pub(crate) locator: Option, /// The mark of the host error ([`crate::HostError::Marked`]) whose exception or limit is /// propagating. pub(crate) thrown_mark: Option, @@ -469,6 +471,7 @@ impl Interp { source_tag: crate::realm::UNKNOWN_SOURCE, evaluated: 0, error_tag: crate::realm::UNKNOWN_SOURCE, + locator: None, thrown_mark: None, finally_marks: Vec::new(), finally_sites: Vec::new(), diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index cad290f1af..1c121a212b 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -83,7 +83,7 @@ pub use error::{SyntaxError, TOO_DEEP, TOO_LONG}; pub use host::{Arg, Data, DataLimits, HostError, HostFunction, HostItem, ScriptItem}; pub use lexer::{Goal, Lexer}; pub use number::to_string as number_to_string; -pub use realm::{Interrupt, JsValue, LimitKind, Limits, MAX_UNBROKEN_FUEL, Progress, Realm, RunError, RunErrorKind, UNKNOWN_SOURCE}; +pub use realm::{Interrupt, JsValue, LimitKind, Limits, MAX_UNBROKEN_FUEL, Progress, Realm, RunError, RunErrorKind, SourceLocator, UNKNOWN_SOURCE}; pub use token::{Directive, DirectiveName, Keyword, Punct, Span, Token, TokenKind}; /// How a script is read. diff --git a/crates/js/src/realm.rs b/crates/js/src/realm.rs index 077f3b19b6..4146ae891b 100644 --- a/crates/js/src/realm.rs +++ b/crates/js/src/realm.rs @@ -63,6 +63,10 @@ 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 file and line that line `line` of the source with tag `tag` ([`Realm::error_tag`]) came +/// from, as `$.line` and `$.stack` give them ([`Realm::set_source_locator`]). +pub type SourceLocator = std::rc::Rc (String, u32)>; + /// 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 { @@ -395,6 +399,13 @@ impl Realm { self.interp.interrupt = interrupt.0.clone(); } + /// Has `$.line` give the line `locate` maps the running line to, and `$.stack` give each + /// frame's line that way, after the file `locate` names: `function@file:line`. Without a + /// locator they give the line of the compiled source, and `$.stack` names no file. + pub fn set_source_locator(&mut self, locate: SourceLocator) { + self.interp.locator = Some(locate); + } + /// Which source the last error's `line` and `column` are in: 0 for the script last given to /// [`Realm::run`] or [`Realm::start`], `n` for the `n`th source a /// [`crate::HostItem::Evaluator`] gave since then, and [`UNKNOWN_SOURCE`] otherwise, which diff --git a/docs/mcp.md b/docs/mcp.md index 274db05a34..7bb5ecd66a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2018,7 +2018,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available: | `global` | the global object | | `hiresTimer` | the microseconds since the run last read it, or since the run started; `0` on the web | | `level`, `memCache` | `0`; setting `memCache` throws | -| `line`, `stack` | the line of the code that reads it; the running code, outermost first, one a line as `name:line` (`[global]` for the script's own code, `[anonymous]` for a function without a name), at most 64 lines | +| `line`, `stack` | the line of the code that reads it, counted in the file that code is in (the script, a file it included or a file `$.evalFile` ran, in this run or an earlier run of its named engine); the running code, outermost first, one a line as `name@file:line` (`[global]` for the script's own code, `[anonymous]` for a function without a name; the file as `$.fileName` gives it, or the run's name for a script given as `source`), at most 64 lines. Code that `eval` or `Function` compiled keeps the line of the source they compiled, in the file `[eval]`; code whose place a named engine no longer keeps is at line `0` of `[unknown]` | | `locale` | the interface's language as a locale such as `de` or `pt_BR` (`en` when the interface follows the system); a script can set `ll` or `ll_RR`, and `null` or `""` sets the interface's again | | `localize` | `false`; while `true`, `String(object)` of a localization object gives its text for `$.locale` | | `os` | `Windows`, `macOS`, `Linux`, `FreeBSD`, `Unix` or `Web`, without a version | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f04ee3c910..e460b24a13 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -783,6 +783,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - 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. 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. `crates/engine/src/tests/script_engines.rs`. + - Scripts: `$.line` is the line of the running script, counting from 1, and `$.stack` is the current stack trace ([the help page](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: both count lines in the file the code is in (the script, a file it included, a file `$.evalFile` ran, or the file of an earlier run of the named engine), and `$.stack` writes each frame as `name@file:line`, outermost first; code that `eval` or `Function` compiled keeps its own line in `[eval]`, and code whose place the engine no longer keeps is at line 0 of `[unknown]`. `crates/engine/src/tests/script_include.rs`, `crates/engine/src/tests/script_engines.rs`, `crates/js/src/extension_tests.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`.