mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 14:46:03 +00:00
Scripts: a kept engine's next run starts after the last uncaught exception is dropped
Resetting Folder.current for the next run of a named engine now drops the last run's uncaught exception and $.error first, so memory only they held no longer makes the reset fail with the heap limit on every later run. A File an earlier run left open now counts as closed in a later run: setting its encoding stores it for the next open, and reading the encoding gives the stored one. docs/mcp.md: engines stay on the thread that made them, and a #targetengine line in a file $.evalFile runs makes that call throw a SyntaxError when it runs.
This commit is contained in:
@@ -151,7 +151,8 @@ fn a_kept_eval_file_reads_in_the_calling_run_scope() {
|
||||
}
|
||||
|
||||
/// `File` and `Folder` objects an earlier run kept read in the folder of the run that calls them,
|
||||
/// `Folder.current` starts at each run's script, and a file an earlier run left open is not open.
|
||||
/// `Folder.current` starts at each run's script, and a file an earlier run left open is not open:
|
||||
/// it takes the encoding it is given, as a closed file does.
|
||||
#[test]
|
||||
fn kept_file_objects_read_in_the_calling_run_folder() {
|
||||
let base: PathBuf = std::env::temp_dir().join(format!("vc-script-engines-files-{}", std::process::id()));
|
||||
@@ -165,11 +166,12 @@ fn kept_file_objects_read_in_the_calling_run_folder() {
|
||||
std::fs::write(&a, "#targetengine e\nvar keep = new File('data.txt');\nkeep.open('r');\nvc.log([Folder.current.name, keep.read(1)]);").unwrap();
|
||||
assert_eq!(s.execute("script.run", &json!({"path": a.to_string_lossy()})).unwrap()["log"], json!([["a", "A"]]));
|
||||
let b = base.join("b").join("main.jsx");
|
||||
let text = "#targetengine e\nvar g = new File('data.txt');\ng.open('r');\nvc.log([Folder.current.name, g.read(), keep.read(), keep.error, keep.exists]);";
|
||||
let text = "#targetengine e\nvar g = new File('data.txt');\ng.open('r');\nvc.log([Folder.current.name, g.read(), keep.read(), keep.error, keep.exists]);\n\
|
||||
var before = keep.encoding;\nkeep.encoding = 'BINARY';\nkeep.open('r');\nvc.log([before, keep.encoding]);";
|
||||
std::fs::write(&b, text).unwrap();
|
||||
let log = s.execute("script.run", &json!({"path": b.to_string_lossy()})).unwrap()["log"].clone();
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
assert_eq!(log, json!([["b", "B", "", "Script file is not open", false]]));
|
||||
assert_eq!(log, json!([["b", "B", "", "Script file is not open", false], ["UTF-8", "BINARY"]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -31,6 +31,8 @@ pub(crate) struct Classes {
|
||||
folder_proto: ObjId,
|
||||
host: ObjId,
|
||||
current: Value,
|
||||
/// The count of [`reset_current`] calls: a file is open only in the run that opened it.
|
||||
run: f64,
|
||||
}
|
||||
|
||||
impl Classes {
|
||||
@@ -43,6 +45,7 @@ impl Classes {
|
||||
const PATH: &str = "__vcPath";
|
||||
const ERROR: &str = "__vcError";
|
||||
const HANDLE: &str = "__vcHandle";
|
||||
const RUN: &str = "__vcRun";
|
||||
const ENCODING: &str = "__vcEncoding";
|
||||
const LINE_FEED: &str = "__vcLineFeed";
|
||||
|
||||
@@ -132,12 +135,13 @@ fn parsed(i: &mut Interp, this: &Value, what: &str, name: &str) -> JsResult<Valu
|
||||
fn instance(i: &mut Interp, proto: ObjId, fs: Value) -> JsResult<Value> {
|
||||
let line_feed = host_value(i, "lineFeed")?;
|
||||
i.rooted(&[fs.clone(), line_feed.clone()], |i| {
|
||||
i.reserve_heap(OBJECT_BYTES + 5 * PROP_BYTES)?;
|
||||
i.reserve_heap(OBJECT_BYTES + 6 * PROP_BYTES)?;
|
||||
let o = i.heap.alloc_object(Object::new(Class::Ordinary, Some(proto)));
|
||||
i.rooted(&[Value::Object(o)], |i| {
|
||||
set_slot(i, o, PATH, fs.clone())?;
|
||||
set_slot(i, o, ERROR, Value::str(""))?;
|
||||
set_slot(i, o, HANDLE, Value::Number(-1.0))?;
|
||||
set_slot(i, o, RUN, Value::Number(-1.0))?;
|
||||
set_slot(i, o, ENCODING, Value::str(""))?;
|
||||
set_slot(i, o, LINE_FEED, line_feed.clone())?;
|
||||
Ok(Value::Object(o))
|
||||
@@ -189,8 +193,7 @@ fn text_result(i: &mut Interp, o: ObjId, r: Value, failed: Value) -> JsResult<Va
|
||||
|
||||
/// The id of the object's open file, or `None` after setting `error` when it has none.
|
||||
fn opened(i: &mut Interp, o: ObjId) -> JsResult<Option<Value>> {
|
||||
let h = slot(i, o, HANDLE)?;
|
||||
if matches!(h, Value::Number(n) if n >= 0.0) {
|
||||
if let Some(h) = is_open(i, o)? {
|
||||
return Ok(Some(h));
|
||||
}
|
||||
let m = message(i, "notOpen")?;
|
||||
@@ -198,9 +201,11 @@ fn opened(i: &mut Interp, o: ObjId) -> JsResult<Option<Value>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The id of the object's open file, when this run opened it.
|
||||
fn is_open(i: &Interp, o: ObjId) -> JsResult<Option<Value>> {
|
||||
let h = slot(i, o, HANDLE)?;
|
||||
Ok(matches!(h, Value::Number(n) if n >= 0.0).then_some(h))
|
||||
let this_run = matches!(slot(i, o, RUN)?, Value::Number(n) if n == classes(i)?.run);
|
||||
Ok((this_run && matches!(h, Value::Number(n) if n >= 0.0)).then_some(h))
|
||||
}
|
||||
|
||||
/// Closes the object's open file, if it has one.
|
||||
@@ -528,6 +533,8 @@ fn open(i: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value>
|
||||
return Ok(Value::Bool(false));
|
||||
}
|
||||
set_slot(i, o, HANDLE, id)?;
|
||||
let run = classes(i)?.run;
|
||||
set_slot(i, o, RUN, Value::Number(run))?;
|
||||
Ok(Value::Bool(true))
|
||||
}
|
||||
|
||||
@@ -759,11 +766,11 @@ fn folder_members(i: &mut Interp, ctor: ObjId, proto: ObjId) -> JsResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets `Folder.current` back to the host's `current`, when the realm has `File` and `Folder`.
|
||||
/// Sets `Folder.current` back to the host's `current`, when the realm has `File` and `Folder`, and
|
||||
/// begins a new run, in which the files earlier runs opened are not open.
|
||||
pub(crate) fn reset_current(i: &mut Interp) -> JsResult<()> {
|
||||
if i.files.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(c) = i.files.as_mut() else { return Ok(()) };
|
||||
c.run += 1.0;
|
||||
let start = host(i, "current", &[])?;
|
||||
if let Some(c) = i.files.as_mut() {
|
||||
c.current = start;
|
||||
@@ -780,7 +787,7 @@ pub(crate) fn install(i: &mut Interp, items: Vec<(String, HostItem)>) -> JsResul
|
||||
i.reserve_heap(2 * OBJECT_BYTES)?;
|
||||
let file_proto = i.heap.alloc_object(Object::new(Class::Ordinary, Some(object_proto)));
|
||||
let folder_proto = i.heap.alloc_object(Object::new(Class::Ordinary, Some(object_proto)));
|
||||
i.files = Some(Classes { file_proto, folder_proto, host: host_obj, current: Value::str("") });
|
||||
i.files = Some(Classes { file_proto, folder_proto, host: host_obj, current: Value::str(""), run: 0.0 });
|
||||
let start = host(i, "current", &[])?;
|
||||
if let Some(c) = i.files.as_mut() {
|
||||
c.current = start;
|
||||
|
||||
@@ -75,6 +75,25 @@ fn an_ended_run_leaves_the_realm_ready_for_the_next() {
|
||||
r.end();
|
||||
}
|
||||
|
||||
/// Setting `Folder.current` back for the next run drops what only the last run's uncaught
|
||||
/// exception held, so a full heap from that exception does not stop the next run.
|
||||
#[test]
|
||||
fn resetting_folder_current_drops_the_last_uncaught_exception() {
|
||||
let mut r = realm(10_000_000);
|
||||
let len = Rc::new(Cell::new(4));
|
||||
let n = len.clone();
|
||||
let current: HostFunction = Rc::new(move |_| Ok(Data::String("d".repeat(n.get()))));
|
||||
r.define_file_classes(vec![("current".into(), HostItem::Function(current))]).unwrap();
|
||||
assert!(r.run("throw (function () { var a = []; for (var i = 0; i < 2000; i++) a.push('item ' + i); return a; })();").is_err());
|
||||
let interp = r.interp();
|
||||
interp.collect_garbage();
|
||||
interp.limits.heap_bytes = interp.heap.bytes;
|
||||
len.set(200);
|
||||
assert_eq!(r.reset_folder_current(), Ok(()));
|
||||
r.set_limits(Limits::default());
|
||||
assert_eq!(r.run("typeof Folder"), Ok(JsValue::String("function".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fuel_limit_covers_every_slice_together() {
|
||||
let mut r = realm(5_000);
|
||||
|
||||
@@ -436,8 +436,13 @@ impl Realm {
|
||||
}
|
||||
|
||||
/// Sets `Folder.current` back to the host's `current`, for the next run of a realm that has
|
||||
/// `File` and `Folder` ([`Realm::define_file_classes`]); a realm without them is left as it is.
|
||||
/// `File` and `Folder` ([`Realm::define_file_classes`]); a realm without them keeps its
|
||||
/// `Folder.current`. The files an earlier run opened are no longer open. The last run's uncaught
|
||||
/// exception and `$.error` are dropped first, as [`Realm::start`] drops them, so memory that only
|
||||
/// they held is free for the reset.
|
||||
pub fn reset_folder_current(&mut self) -> Result<(), RunError> {
|
||||
self.interp.last_thrown = Value::Undefined;
|
||||
self.interp.last_error = Value::Null;
|
||||
match crate::file_classes::reset_current(&mut self.interp) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => self.outcome(Err(e)).map(drop),
|
||||
|
||||
+5
-3
@@ -1723,14 +1723,16 @@ has unsaved changes.
|
||||
### Script engines
|
||||
|
||||
A named engine (`#targetengine`) keeps its globals and functions between the runs that name it, until it is dropped
|
||||
or the session ends. These are VectorCraft's own rules:
|
||||
or the session ends. Engines stay on the thread that made them: a session used on another thread starts there with
|
||||
no engines, and the engines it left are dropped when their own thread next uses engines. These are VectorCraft's own rules:
|
||||
|
||||
- The name is either a name of ASCII letters, digits and `_` that does not start with a digit, or a name in single or
|
||||
double quotes of 1 to 64 bytes of UTF-8 without control characters. Names are matched as written: `Session` and
|
||||
`session` are two engines. `main` is the engine of a script without the line, which is new for each run.
|
||||
- Only the script itself has the line, once, before its code and its `#include` lines; blanks, comments and the other
|
||||
preprocessor lines may come before it. A second line, a late line, a bad name, or the line in an included file or a
|
||||
file `$.evalFile` runs ends the run at that line before any of it runs, and makes no engine.
|
||||
preprocessor lines may come before it. A second line, a late line, a bad name, or the line in an included file ends
|
||||
the run at that line before any of it runs, and makes no engine. In a file `$.evalFile` runs, the line makes that
|
||||
call throw a `SyntaxError` when the call runs.
|
||||
- A session keeps at most 8 named engines. A run that names a ninth ends with a limit error before any of it runs,
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user