From 75c74557bb837906b1d19c2f81372b910e29ce31 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 06:19:23 -0800 Subject: [PATCH] Scripts: $.fileName names the file of the code that reads it $.fileName gave the file being run, so a function defined in a file that #include inserted, in a file $.evalFile ran, or in an earlier run of a named engine reported the calling file instead of its own. - vectorcraft_js: HostItem::SiteGetter gives a getter the CallSite (source tag, line, column) of the code that reads it. - $.fileName maps that place through the run's source maps, or the maps a named engine kept, to the file the code is in. It is empty in code from a script given as source and in kept code whose map was dropped. Code that eval or Function compiled still gets the file being run. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/script/include.rs | 3 +- crates/engine/src/script/mod.rs | 40 ++++++++++++++-- crates/engine/src/tests/script_engines.rs | 46 ++++++++++++++++++ crates/engine/src/tests/script_include.rs | 42 ++++++++++++++++ crates/js/src/host.rs | 58 ++++++++++++++++++++++- crates/js/src/host_tests.rs | 35 ++++++++++++++ crates/js/src/lib.rs | 2 +- docs/mcp.md | 9 +++- docs/target-app-parity.md | 3 +- 9 files changed, 227 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/script/include.rs b/crates/engine/src/script/include.rs index 4ec68dbbde..f1928228df 100644 --- a/crates/engine/src/script/include.rs +++ b/crates/engine/src/script/include.rs @@ -321,7 +321,8 @@ impl Files { std::env::var("JSINCLUDE").map(|v| v.split(';').map(str::trim).filter(|p| !p.is_empty()).map(PathBuf::from).collect()).unwrap_or_default() } - /// The file being run now, as `$.fileName` gives it (empty for a script given as text). + /// The file being run now: the script, or the file `$.evalFile` is running (empty for a script + /// given as text). `$.fileName` gives it to code that `eval` or `Function` compiled. pub(crate) fn file_name(&self) -> String { self.frames.last().map(|f| f.shown.to_string()).unwrap_or_default() } diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 53f6e18f75..22c9d2d4e5 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -822,6 +822,29 @@ impl Host { } } + /// `$.fileName` read by the code at `site`: the file that code is in, by the source map of the + /// source it came from (this run's script, a file `$.evalFile` gave, or code an earlier run of + /// the named engine compiled), so a function keeps the file it was defined in. Code from a file + /// `#include` inserted is in that file. A script given as text has no name, and neither has + /// code whose map the named engine no longer keeps. Code that `eval` or `Function` compiled has + /// no map, and is given the file being run now. + fn file_name_at(&self, site: vectorcraft_js::CallSite) -> String { + let name = |m: &include::SourceMap| m.locate(site.line, site.column).1.to_string(); + let tag = site.source; + if tag == vectorcraft_js::UNKNOWN_SOURCE { + return self.files.borrow().file_name(); + } + let script = self.script_tag.get(); + match tag.checked_sub(script).filter(|_| script != vectorcraft_js::UNKNOWN_SOURCE) { + Some(0) => name(&self.script_map.borrow()), + Some(n) => self.eval_maps.borrow().get(n as usize - 1).map(name).unwrap_or_default(), + None => { + let kept = self.kept_maps.as_ref().map(|k| k.borrow()); + kept.as_ref().and_then(|k| k.get(tag)).map(|k| name(&k.map)).unwrap_or_default() + } + } + } + /// 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 }; @@ -1254,9 +1277,9 @@ fn each_line(text: &str, mut done: impl FnMut(String, bool)) { /// The `$` object: `$.write(...values)` writes the values to the Script Console as `String(value)` /// gives them, one after another, and `$.writeln(...values)` does the same and ends the line. -/// `$.evalFile(path[, timeout])` runs a file and gives its last value, `$.fileName` is the file -/// being run and `$.includePath` the `#includepath` list. Its other members are -/// [`dollar::members`]. +/// `$.evalFile(path[, timeout])` runs a file and gives its last value, `$.fileName` is the file the +/// code reading it is in ([`Host::file_name_at`]) and `$.includePath` the `#includepath` list. Its +/// other members are [`dollar::members`]. fn dollar_object(link: &Link, info: dollar::RunInfo, localization: &Rc) -> Vec<(String, HostItem)> { let writer = |end: bool| { let l = link.clone(); @@ -1278,6 +1301,15 @@ fn dollar_object(link: &Link, info: dollar::RunInfo, localization: &Rc Vec { + std::fs::write(&caller, text).unwrap(); + let r = s.execute("script.run", &json!({"path": caller.to_string_lossy()})).unwrap(); + r["log"][0].as_array().unwrap().iter().map(|v| v.as_str().unwrap().replace('\\', "/")).collect() + }; + let mut s = Session::new(); + s.execute("script.run", &json!({"path": def.to_string_lossy()})).unwrap(); + let from_file = call(&mut s, "#targetengine e\nvc.log([where(), fromInclude(), fromEval(), $.fileName]);"); + let from_text = log(&mut s, "#targetengine e\nvc.log([where(), $.fileName, eval('where()'), eval('$.fileName')]);"); + run(&mut s, "#targetengine e\nfunction fromText() { return $.fileName; }").unwrap(); + let text_kept = call(&mut s, "#targetengine e\nvc.log([fromText(), $.fileName]);"); + // Each run's own place takes the room of the ones before, so def.jsx's places go. + TEST_KEPT_MAP_BYTES.with(|b| b.set(Some(300))); + for _ in 0..20 { + run(&mut s, "#targetengine e\nvar x = 1;").unwrap(); + } + let evicted = call(&mut s, "#targetengine e\nvc.log([where(), fromEval(), $.fileName]);"); + TEST_KEPT_MAP_BYTES.with(|b| b.set(None)); + s.execute("script.engine.drop", &json!({"name": "e"})).unwrap(); + let dropped = log(&mut s, "#targetengine e\nvc.log(typeof where);"); + let _ = std::fs::remove_dir_all(&base); + let ends = |names: &[String], ends: &[&str]| names.len() == ends.len() && names.iter().zip(ends).all(|(n, e)| n.ends_with(e)); + assert!(ends(&from_file, &["a/def.jsx", "a/inc.jsxinc", "a/ev.jsxinc", "b/caller.jsx"]), "{from_file:?}"); + let shown: Vec = from_text[0].as_array().unwrap().iter().map(|v| v.as_str().unwrap().replace('\\', "/")).collect(); + assert!(ends(&shown, &["a/def.jsx", "", "a/def.jsx", ""]) && shown[1].is_empty() && shown[3].is_empty(), "{shown:?}"); + assert!(text_kept[0].is_empty() && text_kept[1].ends_with("b/caller.jsx"), "{text_kept:?}"); + assert!(evicted[0].is_empty() && evicted[1].is_empty() && evicted[2].ends_with("b/caller.jsx"), "{evicted:?}"); + assert_eq!(dropped, json!(["undefined"])); +} + /// 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] diff --git a/crates/engine/src/tests/script_include.rs b/crates/engine/src/tests/script_include.rs index e5acc536cc..1bf259409d 100644 --- a/crates/engine/src/tests/script_include.rs +++ b/crates/engine/src/tests/script_include.rs @@ -374,6 +374,48 @@ fn script_eval_file_returns_last_value_and_shares_globals() { let _ = std::fs::remove_dir_all(base); } +/// `$.fileName` names the file the code reading it is in: a function `$.evalFile` gave names its +/// file after the file has returned, through nested files, and code an included file inserted names +/// that file. Code that `eval` compiled names the file being run. +#[test] +fn script_file_name_names_the_file_of_the_code_that_reads_it() { + let base = folder("filename"); + write(&base.join("main/inc.jsxinc"), "function fromInclude() { return $.fileName; }\nvar includedTop = $.fileName;"); + write( + &base.join("main/sub/outer.jsx"), + "function fromOuter() { return $.fileName; }\nvar before = $.fileName;\n$.evalFile('inner.jsx');\nvar after = $.fileName;\nfromInner();", + ); + write(&base.join("main/sub/inner.jsx"), "function fromInner() { return $.fileName; }\n$.fileName;"); + let r = run( + &base, + "#include \"inc.jsxinc\"\nvar nested = $.evalFile('sub/outer.jsx');\n\ + vc.log([fromInclude(), includedTop, before, after, nested, fromOuter(), fromInner(), $.fileName, eval('$.fileName')]);", + ) + .unwrap(); + let names: Vec = r["log"][0].as_array().unwrap().iter().map(|v| v.as_str().unwrap().replace('\\', "/")).collect(); + let ends = [ + "main/inc.jsxinc", + "main/inc.jsxinc", + "sub/outer.jsx", + "sub/outer.jsx", + "sub/inner.jsx", + "sub/outer.jsx", + "sub/inner.jsx", + "main/main.jsx", + "main/main.jsx", + ]; + assert_eq!(names.len(), ends.len(), "{names:?}"); + for (name, end) in names.iter().zip(ends) { + assert!(name.ends_with(end), "{name} ends with {end}: {names:?}"); + } + // A script given as text has no name, and neither have the functions it defines. + let r = Session::new() + .execute("script.run", &json!({"source": "function f() { return $.fileName; }\nvc.log([f(), $.fileName]);", "name": "t.js"})) + .unwrap(); + assert_eq!(r["log"], json!([["", ""]])); + let _ = std::fs::remove_dir_all(base); +} + #[test] fn script_eval_file_outside_scope_refused() { let base = folder("evaloutside"); diff --git a/crates/js/src/host.rs b/crates/js/src/host.rs index fbb5e4fc90..fb980843e8 100644 --- a/crates/js/src/host.rs +++ b/crates/js/src/host.rs @@ -74,6 +74,21 @@ pub enum HostError { /// A function the host gives a script, with its arguments converted. pub type HostFunction = Rc) -> Result>; +/// The place in the script's code that reads a [`HostItem::SiteGetter`] property: the source tag +/// of that code, as [`crate::Realm::error_tag`] gives one, and the line and column there, counted +/// from 1. A function an earlier script defined keeps its script's tag; code that `eval` or +/// `Function` compiled has [`crate::UNKNOWN_SOURCE`]. A read with none of the script's code +/// running has [`crate::UNKNOWN_SOURCE`] and line and column 0. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CallSite { + pub source: u32, + pub line: u32, + pub column: u32, +} + +/// A getter the host gives a script that is told where the code reading it is. +pub type SiteFunction = Rc Result>; + /// How one argument of a [`HostItem::Converted`] function is read. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Arg { @@ -96,6 +111,8 @@ pub(crate) enum ArgRead { Text, /// One by one as the list says; those past its end are not read. Each(Rc<[Arg]>), + /// None: the function is given the [`CallSite`] of the code that calls it, as three numbers. + Site, } /// A member of an object the host adds to the global environment. @@ -114,6 +131,9 @@ pub enum HostItem { /// A property whose value the function gives each time it is read, frozen as /// [`HostItem::Value`] gives its value. FrozenGetter(HostFunction), + /// A property whose value the function gives each time it is read, given the [`CallSite`] of + /// the code that reads it. + SiteGetter(SiteFunction), /// A property whose value `get` gives each time it is read, and that `set` is given each value /// assigned to it, as its one argument; an error from `set` is thrown to the script. Property { @@ -357,7 +377,10 @@ impl Interp { /// Calls host function `f`: reads its arguments as `how` says, runs it, and gives its result to /// the script. pub(crate) fn call_data(&mut self, f: &crate::heap::DataFn, name: &Rc, args: &[Value], how: &ArgRead) -> JsResult { - let data = self.read_args(name, args, how)?; + let data = match how { + ArgRead::Site => site_data(self.call_site()), + _ => self.read_args(name, args, how)?, + }; // Only the script's own code, in a run that can pause, can wait for a result. let can_wait = self.in_slice && self.reentry == 0; self.can_wait.set(can_wait); @@ -375,6 +398,18 @@ impl Interp { } } + /// Where the innermost frame of the script's code is: its source's tag and the line and column + /// of the instruction it is running. + fn call_site(&self) -> CallSite { + match self.script_frames().last() { + Some((code, pc, _)) => { + let (line, column) = code.line_column(pc.saturating_sub(1)); + CallSite { source: code.source.tag, line, column } + } + None => CallSite { source: crate::UNKNOWN_SOURCE, line: 0, column: 0 }, + } + } + /// Calls evaluator `f` ([`HostItem::Evaluator`]): reads its arguments, runs it, and runs the /// source it gives as global code, as an indirect `eval` does, giving that code's completion /// value. A result that is not a string is given to the script as it is. `end` is called once @@ -442,7 +477,7 @@ impl Interp { for (i, a) in args.iter().enumerate() { reading.path.push(Step::Index(u32::try_from(i).unwrap_or(u32::MAX))); let d = match how { - ArgRead::Data | ArgRead::Frozen => self.read_data(a, &mut reading), + ArgRead::Data | ArgRead::Frozen | ArgRead::Site => self.read_data(a, &mut reading), ArgRead::Text => self.read_text(a, &mut reading), ArgRead::Each(list) => match list.get(i) { Some(Arg::TextOrUndefined) if matches!(a, Value::Undefined) => Ok(Data::Undefined), @@ -531,6 +566,10 @@ impl Interp { HostItem::Value(d) => Prop::Data { value: self.write_data(d, 0, true)?, flags: ENUMERABLE }, HostItem::Getter(f) => Prop::Accessor { get: Some(self.host_function(f, &full, ArgRead::Data)), set: None, flags: ENUMERABLE }, HostItem::FrozenGetter(f) => Prop::Accessor { get: Some(self.host_function(f, &full, ArgRead::Frozen)), set: None, flags: ENUMERABLE }, + HostItem::SiteGetter(f) => { + let get: HostFunction = Rc::new(move |data| f(read_site(&data))); + Prop::Accessor { get: Some(self.host_function(get, &full, ArgRead::Site)), set: None, flags: ENUMERABLE } + } HostItem::Property { get, set } => Prop::Accessor { get: Some(self.host_function(get, &full, ArgRead::Data)), set: Some(self.host_function(set, &full, ArgRead::Data)), @@ -577,3 +616,18 @@ enum Slot { Index(usize), Name(String), } + +/// `site` as the three numbers [`ArgRead::Site`] gives its function. +fn site_data(site: CallSite) -> Vec { + vec![Data::Number(f64::from(site.source)), Data::Number(f64::from(site.line)), Data::Number(f64::from(site.column))] +} + +/// The [`CallSite`] that [`site_data`] gave as `data`. +fn read_site(data: &[Data]) -> CallSite { + let at = |i: usize, missing: u32| match data.get(i) { + // Each number is a `u32` that `site_data` gave, so the conversion is exact. + Some(Data::Number(n)) => *n as u32, + _ => missing, + }; + CallSite { source: at(0, crate::UNKNOWN_SOURCE), line: at(1, 0), column: at(2, 0) } +} diff --git a/crates/js/src/host_tests.rs b/crates/js/src/host_tests.rs index de252c6c64..700efddfb2 100644 --- a/crates/js/src/host_tests.rs +++ b/crates/js/src/host_tests.rs @@ -271,6 +271,41 @@ fn a_frozen_getter_gives_a_frozen_value_each_time() { assert_eq!(r.run(source).unwrap(), JsValue::String("true,true,true,1,false,false".into())); } +/// A site getter is told the source tag, line and column of the code that reads it: a function an +/// earlier script or an evaluator's source defined keeps that source's tag, and code that `eval` +/// compiled has none. +#[test] +fn a_site_getter_is_told_where_the_code_that_reads_it_is() { + let mut r = realm(10_000_000); + let seen: Rc>> = Rc::default(); + let log = seen.clone(); + let site: SiteFunction = Rc::new(move |s| { + log.borrow_mut().push(s); + Ok(Data::Number(f64::from(s.line))) + }); + let source: HostFunction = Rc::new(|_| Ok(Data::String("\n\nfunction inEval() { return host.site; }".into()))); + let load = HostItem::Evaluator { source, end: Rc::new(|| {}), strict_writes: Rc::new(|| None) }; + r.define_host_object("host", vec![("site".into(), HostItem::SiteGetter(site)), ("load".into(), load)]).unwrap(); + r.run("function early() {\n return host.site;\n}\nhost.load();").unwrap(); + let first = r.script_tag(); + assert_eq!(r.run("\n\n\n\nhost.site + early() + inEval() + eval('host.site')").unwrap(), JsValue::Number(5.0 + 2.0 + 3.0 + 1.0)); + let second = r.script_tag(); + let sites = seen.borrow().clone(); + assert_eq!( + sites, + vec![ + CallSite { source: second, line: 5, column: 1 }, + CallSite { source: first, line: 2, column: 11 }, + CallSite { source: first + 1, line: 3, column: 28 }, + CallSite { source: UNKNOWN_SOURCE, line: 1, column: 1 }, + ] + ); + seen.borrow_mut().clear(); + // A getter a built-in calls is at the place of the code that called the built-in. + assert_eq!(r.run("\nObject.getOwnPropertyDescriptor(host, 'site').get.call(host)").unwrap(), JsValue::Number(2.0)); + assert_eq!(seen.borrow().first().map(|s| s.source), Some(r.script_tag())); +} + #[test] fn a_text_function_takes_each_argument_as_string_gives_it() { let mut r = realm(1_000_000); diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index cad290f1af..2e61f2e1bb 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -80,7 +80,7 @@ pub use ast::{ }; pub use builtins::extensions::Localization; pub use error::{SyntaxError, TOO_DEEP, TOO_LONG}; -pub use host::{Arg, Data, DataLimits, HostError, HostFunction, HostItem, ScriptItem}; +pub use host::{Arg, CallSite, Data, DataLimits, HostError, HostFunction, HostItem, ScriptItem, SiteFunction}; 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}; diff --git a/docs/mcp.md b/docs/mcp.md index 274db05a34..1daefda51d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1947,8 +1947,10 @@ app through the global object `vc`, and writes to the Script Console and runs ot file). Its preprocessor lines are read as the script's are. `timeout` is a number of milliseconds from 0; the file is read at once and `timeout` is not waited on. The run's fuel and limits apply to the file. An empty path, a missing file and a refused path throw errors the script can catch. -- `$.fileName` is the file being run, which is the file `$.evalFile` is running while it runs, and is empty for a - script given as `source`. `$.includePath` is the current `#includepath` list, separated by `;`. `$.engineName` is +- `$.fileName` is the file of the code that reads it: the script, a file it included with `#include`, or a file + `$.evalFile` ran, also when a function that file defined is called after `$.evalFile` returned. It is empty in code + from a script given as `source`. In code that `eval` or `Function` compiled it is the file being run, which is the + file `$.evalFile` is running while it runs. `$.includePath` is the current `#includepath` list, separated by `;`. `$.engineName` is the name `#targetengine` gave the engine the script runs in (`VectorCraft` without the line) and is read-only. - `alert(message[, title, errorIcon])`, `confirm(message[, noAsDflt, title])` and `prompt(message, preset[, title])` open a dialog, described below. `vc.api.has("dialogs")` is true. @@ -2092,6 +2094,9 @@ no engines, and the engines it left are dropped when their own thread next uses for 8 MiB of source maps, the oldest going first, and a run whose script does not compile keeps none. 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. +- `$.fileName` in code an earlier run compiled names the file that code is in, by the same places: the earlier run's + script, a file it included or a file its `$.evalFile` ran, not the calling run's file. It is empty in code from an + earlier script given as `source`, even when that run had a name, and in code whose place is gone. - `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`, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f04ee3c910..f0d1ff4fcf 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -780,8 +780,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Scripts: an error `$.evalFile` throws for a file it cannot run is reported at that file's line and column only while no `catch` has taken it. Caught and thrown again later, in the script or in another file, it is reported where it was thrown again, and a new error whose message holds its text is reported where that error was thrown. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`. - Scripts: an uncaught error that one or more `finally` blocks pass on unchanged, in the script or in a function it called, is reported at the line and column where it was thrown, in `script.run`'s report and the Script Console. An error a `finally` block throws is reported at that throw, and one its `return`, `break` or `continue` discards is not reported. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`. - 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: 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 current script's file 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: `$.fileName` is a read-only name of the script file in use ([the help page](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: it names the file of the code that reads it, so a function a file included with `#include` or run with `$.evalFile` defined, or one an earlier run of a named engine kept, gives its own file and not the calling file; code from a script given as `source`, and kept code whose place the engine dropped, gives an empty name, and code that `eval` or `Function` compiled gives the file being run. `crates/engine/src/tests/script_include.rs`, `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: `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`).