Scripts: an error $.evalFile could not run, caught and thrown again, is reported where it was thrown again

A failure $.evalFile raised for a file it could not run was placed by
matching the uncaught error's message against the last such failure. A
script that caught the failure and threw it again later, or threw a new
error whose message held its text, was reported at the failed file's line
instead of the second throw.

The failure now carries a mark (HostError::Marked) with the exception.
A catch clause drops the mark, a throw raises the value without one, and
a finally block that ends normally throws it on with its mark, nested
finally blocks included. Realm::error_mark gives the mark of the error a
run ended with, and the engine looks up the file, line and column by it.
This commit is contained in:
dotCooCoo
2026-11-11 03:14:42 -08:00
parent 4e0e1c8af7
commit df5d061b30
8 changed files with 170 additions and 26 deletions
+28 -23
View File
@@ -496,14 +496,11 @@ struct Host {
script_map: RefCell<include::SourceMap>,
/// The same for each source `$.evalFile` gave, in order.
eval_maps: RefCell<Vec<include::SourceMap>>,
/// The last error `$.evalFile` raised for a file it could not run, with the file, line and
/// column that error is at.
eval_failure: RefCell<Option<EvalFailure>>,
/// 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)>>,
}
/// An error `$.evalFile` raised, as thrown, and the file, line and column it is at.
type EvalFailure = (String, Arc<str>, u32, u32);
/// A run's host calls by kind, as its report gives them in `hostCallCounts`.
#[derive(Default)]
struct Counts {
@@ -580,6 +577,15 @@ impl Host {
HostError::Abort(limit.to_string())
}
/// `error`, marked as an error at `file`, `line` and `column`, the place [`located`] gives when
/// the run ends with it uncaught.
fn placed(&self, error: HostError, file: Arc<str>, line: u32, column: u32) -> HostError {
let mut places = self.eval_failures.borrow_mut();
let Ok(mark) = u32::try_from(places.len()) else { return error };
places.push((file, line, column));
HostError::Marked { error: Box::new(error), mark }
}
fn charge_call(&self, bytes: u64) -> std::result::Result<(), HostError> {
let calls = self.calls.get() + 1;
self.calls.set(calls);
@@ -987,14 +993,12 @@ fn eval_file(host: &Host, args: Vec<Data>) -> std::result::Result<Data, HostErro
host.charge_call(path.len() as u64 + 2)?;
let expanded = host.files.borrow_mut().eval_file(path).map_err(|f| {
let message = if f.line > 0 { format!("{}:{}:{}: {}", f.file, f.line, f.column, f.message) } else { f.message };
if f.line > 0 {
*host.eval_failure.borrow_mut() = Some((message.clone(), f.file.clone(), f.line, f.column));
}
match f.kind {
let error = match f.kind {
include::FailKind::Limit => HostError::Abort(message),
include::FailKind::Syntax => HostError::Syntax(message),
include::FailKind::Error => HostError::Error(message),
}
};
if f.line > 0 { host.placed(error, f.file, f.line, f.column) } else { error }
})?;
// A file without preprocessor lines is parsed here first, so its syntax error names its place.
let options = vectorcraft_js::Options { max_source_units: include::MAX_TOTAL_BYTES, ..vectorcraft_js::Options::default() };
@@ -1002,8 +1006,7 @@ fn eval_file(host: &Host, args: Vec<Data>) -> std::result::Result<Data, HostErro
host.files.borrow_mut().end_eval();
let (_, file, line, column) = expanded.map.locate(e.line, e.column);
let message = format!("{file}:{line}:{column}: {}", e.message);
*host.eval_failure.borrow_mut() = Some((message.clone(), file, line, column));
return Err(HostError::Syntax(message));
return Err(host.placed(HostError::Syntax(message), file, line, column));
}
host.eval_maps.borrow_mut().push(expanded.map);
Ok(Data::String(expanded.source))
@@ -1095,18 +1098,20 @@ 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`]), 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, e: &RunError) -> (RunError, Option<Arc<str>>) {
/// `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| {
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))
};
// An error `$.evalFile` raised for a file it could not run is at that file's place.
if let Some((_, file, line, column)) = host.eval_failure.borrow().as_ref().filter(|(thrown, ..)| e.message.contains(thrown.as_str())) {
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: *line, column: *column }, Some(file.clone()));
// 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()) {
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);
@@ -1301,7 +1306,7 @@ impl Session {
files: RefCell::new(files),
script_map: RefCell::default(),
eval_maps: RefCell::default(),
eval_failure: RefCell::default(),
eval_failures: RefCell::default(),
});
let args = to_data(&Value::Object(request.args), &DataLimits::default(), 0, &mut (0, 0))
.map_err(|place| EngineError::BadParams { cmd: "script.run".into(), msg: format!("`args{place}` is nested more than 64 levels deep") })?;
@@ -1316,7 +1321,7 @@ impl Session {
.and_then(|()| realm.define_host_object("$", dollar_object(&host)))
.and_then(|()| dialogs::install(&mut realm, &host))
.and_then(|()| realm.start(&expanded.source))
.map_err(|e| located(&host, realm.error_source(), &e))
.map_err(|e| located(&host, realm.error_source(), realm.error_mark(), &e))
}
};
if let Err((e, file)) = started {
@@ -1388,7 +1393,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(), e);
let (e, file) = located(&job.host, job.realm.error_source(), job.realm.error_mark(), 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));
drop(job);