Merge pull request #7147 from dotCooCoo/fix/script-file-name

Scripts: $.fileName names the file of the code that reads it
This commit is contained in:
BFlat
2026-11-13 23:04:04 +07:00
committed by GitHub
9 changed files with 227 additions and 11 deletions
+2 -1
View File
@@ -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()
}
+36 -4
View File
@@ -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<vectorcraft_js::Localization>) -> 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<vectorcra
Ok(Data::String(text))
}))
};
let file_name = {
let l = link.clone();
HostItem::SiteGetter(Rc::new(move |site| {
let h = current(&l)?;
let text = h.file_name_at(site);
h.charge_call(text.len() as u64 + 2)?;
Ok(Data::String(text))
}))
};
let eval_file = {
let (l, end, strict) = (link.clone(), link.clone(), link.clone());
HostItem::Evaluator {
@@ -1294,7 +1326,7 @@ fn dollar_object(link: &Link, info: dollar::RunInfo, localization: &Rc<vectorcra
("write".into(), writer(false)),
("writeln".into(), writer(true)),
("evalFile".into(), eval_file),
("fileName".into(), getter(include::Files::file_name)),
("fileName".into(), file_name),
("includePath".into(), getter(include::Files::include_path)),
];
items.extend(dollar::members(link, info, localization));
+46
View File
@@ -202,6 +202,52 @@ fn an_error_in_code_an_earlier_run_kept_is_at_that_runs_file_and_line() {
let _ = std::fs::remove_dir_all(base);
}
/// `$.fileName` in code an earlier run of a named engine kept names the file that code is in: that
/// run's script, a file it included or a file `$.evalFile` gave, not the file of the run that calls
/// it. Code from a script given as text, and code whose place the engine no longer keeps, has no
/// name; a dropped engine takes its functions with it.
#[test]
fn a_kept_function_reads_the_file_name_of_the_file_it_is_in() {
let base: PathBuf = std::env::temp_dir().join(format!("vc-script-engines-file-name-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
for dir in ["a", "b"] {
std::fs::create_dir_all(base.join(dir)).unwrap();
}
std::fs::write(base.join("a/inc.jsxinc"), "function fromInclude() { return $.fileName; }").unwrap();
std::fs::write(base.join("a/ev.jsxinc"), "\nfunction fromEval() { return $.fileName; }").unwrap();
let def = base.join("a/def.jsx");
std::fs::write(&def, "#targetengine e\nfunction where() { return $.fileName; }\n#include \"inc.jsxinc\"\n$.evalFile('ev.jsxinc');").unwrap();
let caller = base.join("b/caller.jsx");
let call = |s: &mut Session, text: &str| -> Vec<String> {
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<String> = 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]
+42
View File
@@ -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<String> = 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");
+56 -2
View File
@@ -74,6 +74,21 @@ pub enum HostError {
/// A function the host gives a script, with its arguments converted.
pub type HostFunction = Rc<dyn Fn(Vec<Data>) -> Result<Data, HostError>>;
/// 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<dyn Fn(CallSite) -> Result<Data, HostError>>;
/// 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<str>, args: &[Value], how: &ArgRead) -> JsResult<Value> {
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<Data> {
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) }
}
+35
View File
@@ -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<RefCell<Vec<CallSite>>> = 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);
+1 -1
View File
@@ -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};
+7 -2
View File
@@ -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`,
+2 -1
View File
@@ -783,8 +783,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`).