Преглед изворни кода

wip: update the interpreter to return a new type of RuntimeError containing a backtrace. The backtrace is not yet implemented

Tamo пре 1 недеља
родитељ
комит
3c10cdd7ab

+ 6 - 3
numbat-cli/src/main.rs

@@ -16,7 +16,7 @@ use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystem
 use numbat::pretty_print::PrettyPrint;
 use numbat::resolver::CodeSource;
 use numbat::session_history::{ParseEvaluationResult, SessionHistory};
-use numbat::{markup as m, RuntimeError};
+use numbat::{markup as m, RuntimeError, RuntimeErrorKind};
 use numbat::{Context, NumbatError};
 use numbat::{InterpreterSettings, NameResolutionError};
 
@@ -379,11 +379,14 @@ impl Cli {
                     }
 
                     rl.add_history_entry(&line)?;
+                    let mut ctx = self.context.lock().unwrap();
+
                     if interactive && rl.append_history(history_path).is_err() {
-                        self.print_diagnostic(RuntimeError::HistoryWrite(history_path.to_owned()));
+                        ctx.print_diagnostic(ctx.runtime_error(RuntimeErrorKind::HistoryWrite(
+                            history_path.to_owned(),
+                        )));
                     }
 
-                    let mut ctx = self.context.lock().unwrap();
                     match cmd_runner.try_run_command(&line, &mut ctx, rl) {
                         Ok(cf) => match cf {
                             CommandControlFlow::Continue => continue,

+ 10 - 2
numbat/src/bytecode_interpreter.rs

@@ -7,7 +7,7 @@ use crate::ast::ProcedureKind;
 use crate::decorator::Decorator;
 use crate::dimension::DimensionRegistry;
 use crate::interpreter::{
-    Interpreter, InterpreterResult, InterpreterSettings, Result, RuntimeError,
+    Interpreter, InterpreterResult, InterpreterSettings, Result, RuntimeError, RuntimeErrorKind,
 };
 use crate::name_resolution::LAST_RESULT_IDENTIFIERS;
 use crate::prefix::Prefix;
@@ -48,6 +48,11 @@ pub struct BytecodeInterpreter {
 }
 
 impl BytecodeInterpreter {
+    /// Return a runtime error on the current instruction.
+    pub fn runtime_error(&self, kind: RuntimeErrorKind) -> RuntimeError {
+        self.vm.runtime_error(kind)
+    }
+
     fn compile_expression(&mut self, expr: &Expression) {
         match expr {
             Expression::Scalar(span, n, _type) => {
@@ -420,7 +425,10 @@ impl BytecodeInterpreter {
                             metric_prefixes: decorators.contains(&Decorator::MetricPrefixes),
                         },
                     )
-                    .map_err(RuntimeError::UnitRegistryError)?;
+                    .map_err(|e| {
+                        self.vm
+                            .runtime_error(RuntimeErrorKind::UnitRegistryError(e))
+                    })?;
 
                 let constant_idx = self.vm.add_constant(Constant::Unit(Unit::new_base(
                     unit_name.to_compact_string(),

+ 5 - 2
numbat/src/command.rs

@@ -5,6 +5,7 @@ use compact_str::ToCompactString;
 use crate::{
     diagnostic::ErrorDiagnostic,
     help::help_markup,
+    interpreter::RuntimeErrorKind,
     markup::{self as m, Markup},
     parser::ParseErrorKind,
     resolver::CodeSource,
@@ -120,7 +121,7 @@ struct SaveCmdArgs<'session, 'input> {
 }
 
 impl SaveCmdArgs<'_, '_> {
-    fn save(&self) -> Result<(), Box<RuntimeError>> {
+    fn save(&self) -> Result<(), Box<RuntimeErrorKind>> {
         let Self {
             session_history,
             dst,
@@ -403,7 +404,9 @@ impl<Editor> CommandRunner<Editor> {
             }
             Command::Clear { clear_fn } => clear_fn(editor),
             Command::Save(save_args) => {
-                save_args.save().map_err(|err| Box::new((*err).into()))?;
+                save_args
+                    .save()
+                    .map_err(|err| CommandError::Runtime(ctx.interpreter.runtime_error(*err)))?;
                 CommandControlFlow::Continue
             }
             Command::Reset { ctx_ctor, clear_fn } => {

+ 5 - 5
numbat/src/diagnostic.rs

@@ -1,7 +1,7 @@
 use codespan_reporting::diagnostic::LabelStyle;
 
 use crate::{
-    interpreter::RuntimeError,
+    interpreter::{RuntimeError, RuntimeErrorKind},
     parser::ParseError,
     pretty_print::PrettyPrint,
     resolver::ResolverError,
@@ -494,13 +494,13 @@ impl ErrorDiagnostic for RuntimeError {
     fn diagnostics(&self) -> Vec<Diagnostic> {
         let inner = format!("{self:#}");
 
-        match self {
-            RuntimeError::AssertFailed(span) => vec![Diagnostic::error()
+        match &self.kind {
+            RuntimeErrorKind::AssertFailed(span) => vec![Diagnostic::error()
                 .with_message("assertion failed")
                 .with_labels(vec![span
                     .diagnostic_label(LabelStyle::Primary)
                     .with_message("assertion failed")])],
-            RuntimeError::AssertEq2Failed(assert_eq2_error) => {
+            RuntimeErrorKind::AssertEq2Failed(assert_eq2_error) => {
                 vec![Diagnostic::error()
                     .with_message("Assertion failed")
                     .with_labels(vec![
@@ -515,7 +515,7 @@ impl ErrorDiagnostic for RuntimeError {
                     ])
                     .with_notes(vec![inner])]
             }
-            RuntimeError::AssertEq3Failed(assert_eq3_error) => {
+            RuntimeErrorKind::AssertEq3Failed(assert_eq3_error) => {
                 let (lhs, rhs) = assert_eq3_error.fmt_comparands();
 
                 vec![Diagnostic::error()

+ 2 - 1
numbat/src/ffi/currency.rs

@@ -2,10 +2,11 @@ use super::macros::*;
 use super::Args;
 use super::Result;
 use crate::currency::ExchangeRatesCache;
+use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::value::Value;
 
-pub fn exchange_rate(mut args: Args) -> Result<Value> {
+pub fn exchange_rate(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let rate = string_arg!(args);
 
     let exchange_rates = ExchangeRatesCache::new();

+ 19 - 19
numbat/src/ffi/datetime.rs

@@ -10,25 +10,25 @@ use super::macros::*;
 use super::Args;
 use super::Result;
 use crate::datetime;
+use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::value::FunctionReference;
 use crate::value::Value;
-use crate::RuntimeError;
 
-pub fn now(_args: Args) -> Result<Value> {
+pub fn now(_args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     return_datetime!(Zoned::now())
 }
 
-pub fn datetime(mut args: Args) -> Result<Value> {
+pub fn datetime(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let input = string_arg!(args);
 
     let output = datetime::parse_datetime(&input)
-        .map_err(|e| RuntimeError::DateParsingError(e.to_string()))?;
+        .map_err(|e| RuntimeErrorKind::DateParsingError(e.to_string()))?;
 
     return_datetime!(output)
 }
 
-pub fn format_datetime(mut args: Args) -> Result<Value> {
+pub fn format_datetime(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let format = string_arg!(args);
     let dt = datetime_arg!(args);
 
@@ -38,19 +38,19 @@ pub fn format_datetime(mut args: Args) -> Result<Value> {
         // into a jiff::fmt::Write, which is necessary to write a formatted datetime
         // into it
         .format(&format, StdFmtWrite(&mut output))
-        .map_err(|e| RuntimeError::DateFormattingError(e.to_string()))?;
+        .map_err(|e| RuntimeErrorKind::DateFormattingError(e.to_string()))?;
 
     return_string!(owned = output)
 }
 
-pub fn get_local_timezone(_args: Args) -> Result<Value> {
+pub fn get_local_timezone(_args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let local_tz = datetime::get_local_timezone_or_utc();
     let tz_name = local_tz.iana_name().unwrap_or("<unknown timezone>");
 
     return_string!(borrowed = tz_name)
 }
 
-pub fn tz(mut args: Args) -> Result<Value> {
+pub fn tz(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let tz = string_arg!(args);
 
     Ok(Value::FunctionReference(FunctionReference::TzConversion(
@@ -58,7 +58,7 @@ pub fn tz(mut args: Args) -> Result<Value> {
     )))
 }
 
-pub fn unixtime(mut args: Args) -> Result<Value> {
+pub fn unixtime(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let input = datetime_arg!(args);
 
     let output = input.timestamp().as_second();
@@ -66,11 +66,11 @@ pub fn unixtime(mut args: Args) -> Result<Value> {
     return_scalar!(output as f64)
 }
 
-pub fn from_unixtime(mut args: Args) -> Result<Value> {
+pub fn from_unixtime(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let timestamp = quantity_arg!(args).unsafe_value().to_f64() as i64;
 
     let dt = Timestamp::from_second(timestamp)
-        .map_err(|_| RuntimeError::DateTimeOutOfRange)?
+        .map_err(|_| RuntimeErrorKind::DateTimeOutOfRange)?
         .to_zoned(datetime::get_local_timezone_or_utc());
 
     return_datetime!(dt)
@@ -80,35 +80,35 @@ fn calendar_add(
     mut args: Args,
     unit_name: &str,
     to_span: fn(i64) -> std::result::Result<Span, jiff::Error>,
-) -> Result<Value> {
+) -> Result<Value, Box<RuntimeErrorKind>> {
     let dt = datetime_arg!(args);
     let n = quantity_arg!(args).unsafe_value().to_f64();
 
     if n.fract() != 0.0 {
-        return Err(Box::new(RuntimeError::UserError(format!(
+        return Err(Box::new(RuntimeErrorKind::UserError(format!(
             "calendar_add: requires an integer number of {unit_name}s"
         ))));
     }
 
     let n_i64 = n.to_i64().ok_or_else(|| {
-        RuntimeError::UserError(format!("calendar:add: number of {unit_name}s is too large",))
+        RuntimeErrorKind::UserError(format!("calendar:add: number of {unit_name}s is too large",))
     })?;
 
     let output = dt
-        .checked_add(to_span(n_i64).map_err(|_| RuntimeError::DurationOutOfRange)?)
-        .map_err(|_| RuntimeError::DateTimeOutOfRange)?;
+        .checked_add(to_span(n_i64).map_err(|_| RuntimeErrorKind::DurationOutOfRange)?)
+        .map_err(|_| RuntimeErrorKind::DateTimeOutOfRange)?;
 
     return_datetime!(output)
 }
 
-pub fn _add_days(args: Args) -> Result<Value> {
+pub fn _add_days(args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     calendar_add(args, "day", |n| Span::new().try_days(n))
 }
 
-pub fn _add_months(args: Args) -> Result<Value> {
+pub fn _add_months(args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     calendar_add(args, "month", |n| Span::new().try_months(n))
 }
 
-pub fn _add_years(args: Args) -> Result<Value> {
+pub fn _add_years(args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     calendar_add(args, "year", |n| Span::new().try_years(n))
 }

+ 8 - 8
numbat/src/ffi/functions.rs

@@ -2,7 +2,7 @@ use std::collections::HashMap;
 use std::sync::OnceLock;
 
 use super::{macros::*, Args};
-use crate::{quantity::Quantity, value::Value, RuntimeError};
+use crate::{interpreter::RuntimeErrorKind, quantity::Quantity, value::Value, RuntimeError};
 
 use super::{Callable, ForeignFunction, Result};
 
@@ -118,38 +118,38 @@ pub(crate) fn functions() -> &'static HashMap<&'static str, ForeignFunction> {
     })
 }
 
-fn error(mut args: Args) -> Result<Value> {
-    Err(Box::new(RuntimeError::UserError(
+fn error(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
+    Err(Box::new(RuntimeErrorKind::UserError(
         arg!(args).unsafe_as_string().to_string(),
     )))
 }
 
-fn value_of(mut args: Args) -> Result<Value> {
+fn value_of(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let quantity = quantity_arg!(args);
 
     return_scalar!(quantity.unsafe_value().to_f64())
 }
 
-fn has_unit(mut args: Args) -> Result<Value> {
+fn has_unit(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let quantity = quantity_arg!(args);
     let unit_query = quantity_arg!(args);
 
     return_boolean!(quantity.is_zero() || quantity.unit() == unit_query.unit())
 }
 
-fn is_dimensionless(mut args: Args) -> Result<Value> {
+fn is_dimensionless(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let quantity = quantity_arg!(args);
 
     return_boolean!(quantity.as_scalar().is_ok())
 }
 
-fn unit_name(mut args: Args) -> Result<Value> {
+fn unit_name(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let quantity = quantity_arg!(args);
 
     return_string!(from = &quantity.unit().to_string())
 }
 
-fn quantity_cast(mut args: Args) -> Result<Value> {
+fn quantity_cast(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let value_from = quantity_arg!(args);
     let _ = quantity_arg!(args);
 

+ 7 - 7
numbat/src/ffi/lists.rs

@@ -1,33 +1,33 @@
 use super::macros::*;
 use super::{Args, Result};
+use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::value::Value;
-use crate::RuntimeError;
 
-pub fn len(mut args: Args) -> Result<Value> {
+pub fn len(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let list = list_arg!(args);
 
     return_scalar!(list.len() as f64)
 }
 
-pub fn head(mut args: Args) -> Result<Value> {
+pub fn head(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let list = list_arg!(args);
 
     if let Some(first) = list.head() {
         Ok(first)
     } else {
-        Err(Box::new(RuntimeError::EmptyList))
+        Err(Box::new(RuntimeErrorKind::EmptyList))
     }
 }
 
-pub fn tail(mut args: Args) -> Result<Value> {
+pub fn tail(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let mut list = list_arg!(args);
 
     list.tail()?;
     Ok(list.into())
 }
 
-pub fn cons(mut args: Args) -> Result<Value> {
+pub fn cons(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let element = arg!(args);
     let mut list = list_arg!(args);
     list.push_front(element);
@@ -35,7 +35,7 @@ pub fn cons(mut args: Args) -> Result<Value> {
     return_list!(list)
 }
 
-pub fn cons_end(mut args: Args) -> Result<Value> {
+pub fn cons_end(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let element = arg!(args);
     let mut list = list_arg!(args);
     list.push_back(element);

+ 3 - 2
numbat/src/ffi/lookup.rs

@@ -3,12 +3,13 @@ use compact_str::CompactString;
 use super::macros::*;
 use super::Args;
 use super::Result;
+use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::typed_ast::DType;
 use crate::value::Value;
 use crate::RuntimeError;
 
-pub fn _get_chemical_element_data_raw(mut args: Args) -> Result<Value> {
+pub fn _get_chemical_element_data_raw(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     use crate::span::{ByteIndex, Span};
     use crate::typed_ast::StructInfo;
     use crate::typed_ast::Type;
@@ -142,7 +143,7 @@ pub fn _get_chemical_element_data_raw(mut args: Args) -> Result<Value> {
             ],
         ))
     } else {
-        Err(Box::new(RuntimeError::ChemicalElementNotFound(
+        Err(Box::new(RuntimeErrorKind::ChemicalElementNotFound(
             pattern.to_string(),
         )))
     }

+ 9 - 8
numbat/src/ffi/math.rs

@@ -2,10 +2,11 @@ use super::macros::*;
 use super::Args;
 use super::Result;
 
+use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::value::Value;
 
-pub fn mod_(mut args: Args) -> Result<Value> {
+pub fn mod_(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let x = quantity_arg!(args);
     let y = quantity_arg!(args);
 
@@ -18,14 +19,14 @@ pub fn mod_(mut args: Args) -> Result<Value> {
 // A simple math function with signature 'Fn[(Scalar) -> Scalar]'
 macro_rules! simple_scalar_math_function {
     ($name:ident, $op:ident) => {
-        pub fn $name(mut args: Args) -> Result<Value> {
+        pub fn $name(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
             let value = scalar_arg!(args).to_f64();
             return_scalar!(value.$op())
         }
     };
 }
 
-pub fn abs(mut args: Args) -> Result<Value> {
+pub fn abs(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let arg = quantity_arg!(args);
     return_quantity!(arg.unsafe_value().to_f64().abs(), arg.unit().clone())
 }
@@ -43,7 +44,7 @@ simple_scalar_math_function!(asin, asin);
 simple_scalar_math_function!(acos, acos);
 simple_scalar_math_function!(atan, atan);
 
-pub fn atan2(mut args: Args) -> Result<Value> {
+pub fn atan2(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let y = quantity_arg!(args);
     let x = quantity_arg!(args);
 
@@ -64,24 +65,24 @@ simple_scalar_math_function!(ln, ln);
 simple_scalar_math_function!(log10, log10);
 simple_scalar_math_function!(log2, log2);
 
-pub fn gamma(mut args: Args) -> Result<Value> {
+pub fn gamma(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let input = scalar_arg!(args).to_f64();
 
     return_scalar!(crate::gamma::gamma(input))
 }
 
-pub fn is_nan(mut args: Args) -> Result<Value> {
+pub fn is_nan(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let arg = quantity_arg!(args);
 
     return_boolean!(arg.unsafe_value().to_f64().is_nan())
 }
 
-pub fn is_infinite(mut args: Args) -> Result<Value> {
+pub fn is_infinite(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let arg = quantity_arg!(args);
 
     return_boolean!(arg.unsafe_value().to_f64().is_infinite())
 }
 
-pub fn random(_args: Args) -> Result<Value> {
+pub fn random(_args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     return_scalar!(rand::random::<f64>())
 }

+ 4 - 4
numbat/src/ffi/mod.rs

@@ -11,21 +11,21 @@ mod strings;
 
 use std::collections::VecDeque;
 
-use crate::interpreter::RuntimeError;
+use crate::interpreter::{RuntimeError, RuntimeErrorKind};
 use crate::span::Span;
 use crate::value::Value;
 use crate::vm::ExecutionContext;
 
-type ControlFlow = std::ops::ControlFlow<RuntimeError>;
+type ControlFlow = std::ops::ControlFlow<RuntimeErrorKind>;
 
 pub(crate) type ArityRange = std::ops::RangeInclusive<usize>;
 
-type Result<T> = std::result::Result<T, Box<RuntimeError>>;
+type Result<T, E = Box<RuntimeError>> = std::result::Result<T, E>;
 
 pub(crate) type Args = VecDeque<Value>;
 
 pub(crate) enum Callable {
-    Function(fn(Args) -> Result<Value>),
+    Function(fn(Args) -> Result<Value, Box<RuntimeErrorKind>>),
     Procedure(fn(&mut ExecutionContext, Args, Vec<Span>) -> ControlFlow),
 }
 

+ 8 - 4
numbat/src/ffi/plot.rs

@@ -1,6 +1,9 @@
 #[cfg(feature = "plotting")]
 use plotly::Plot;
 
+#[cfg(feature = "plotting")]
+use crate::interpreter::RuntimeErrorKind;
+
 #[cfg(feature = "plotting")]
 use super::macros::*;
 
@@ -10,7 +13,6 @@ use compact_str::CompactString;
 use super::Args;
 use super::Result;
 use crate::value::Value;
-use crate::RuntimeError;
 
 #[cfg(feature = "plotting")]
 fn line_plot(mut args: Args) -> Plot {
@@ -96,11 +98,13 @@ fn show_plot(plot: Plot) -> CompactString {
 }
 
 #[cfg(feature = "plotting")]
-pub fn show(args: Args) -> Result<Value> {
+pub fn show(args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     // Dynamic dispatch hack since we don't have bounded polymorphism.
     // And no real support for generics in the FFI.
+
+    use crate::interpreter::RuntimeErrorKind;
     let Value::StructInstance(info, _) = args.front().unwrap() else {
-        return Err(Box::new(RuntimeError::UserError(
+        return Err(Box::new(RuntimeErrorKind::UserError(
             "Unsupported argument to 'show'.".into(),
         )));
     };
@@ -110,7 +114,7 @@ pub fn show(args: Args) -> Result<Value> {
     } else if info.name == "BarChart" {
         bar_chart(args)
     } else {
-        return Err(Box::new(RuntimeError::UserError(format!(
+        return Err(Box::new(RuntimeErrorKind::UserError(format!(
             "Unsupported plot type: {}",
             info.name
         ))));

+ 10 - 7
numbat/src/ffi/procedures.rs

@@ -6,7 +6,10 @@ use super::macros::*;
 use crate::{
     ast::ProcedureKind,
     ffi::ControlFlow,
-    interpreter::assert_eq::{AssertEq2Error, AssertEq3Error},
+    interpreter::{
+        assert_eq::{AssertEq2Error, AssertEq3Error},
+        RuntimeErrorKind,
+    },
     pretty_print::PrettyPrint,
     span::Span,
     value::Value,
@@ -70,7 +73,7 @@ fn assert(_: &mut ExecutionContext, mut args: Args, arg_spans: Vec<Span>) -> Con
     if arg!(args).unsafe_as_bool() {
         ControlFlow::Continue(())
     } else {
-        ControlFlow::Break(RuntimeError::AssertFailed(arg_spans[0]))
+        ControlFlow::Break(RuntimeErrorKind::AssertFailed(arg_spans[0]))
     }
 }
 
@@ -84,7 +87,7 @@ fn assert_eq(_: &mut ExecutionContext, mut args: Args, arg_spans: Vec<Span>) ->
         let lhs = arg!(args);
         let rhs = arg!(args);
 
-        let error = ControlFlow::Break(RuntimeError::AssertEq2Failed(AssertEq2Error {
+        let error = ControlFlow::Break(RuntimeErrorKind::AssertEq2Failed(AssertEq2Error {
             span_lhs,
             lhs: lhs.clone(),
             span_rhs,
@@ -116,25 +119,25 @@ fn assert_eq(_: &mut ExecutionContext, mut args: Args, arg_spans: Vec<Span>) ->
 
         let lhs_converted = lhs_original.convert_to(eps.unit());
         let lhs_converted = match lhs_converted {
-            Err(e) => return ControlFlow::Break(RuntimeError::QuantityError(e)),
+            Err(e) => return ControlFlow::Break(RuntimeErrorKind::QuantityError(e)),
             Ok(q) => q,
         };
         let rhs_converted = rhs_original.convert_to(eps.unit());
         let rhs_converted = match rhs_converted {
-            Err(e) => return ControlFlow::Break(RuntimeError::QuantityError(e)),
+            Err(e) => return ControlFlow::Break(RuntimeErrorKind::QuantityError(e)),
             Ok(q) => q,
         };
 
         let result = &lhs_converted - &rhs_converted;
 
         match result {
-            Err(e) => ControlFlow::Break(RuntimeError::QuantityError(e)),
+            Err(e) => ControlFlow::Break(RuntimeErrorKind::QuantityError(e)),
             Ok(diff) => {
                 let diff_abs = diff.abs();
                 if diff_abs <= eps {
                     ControlFlow::Continue(())
                 } else {
-                    ControlFlow::Break(RuntimeError::AssertEq3Failed(AssertEq3Error {
+                    ControlFlow::Break(RuntimeErrorKind::AssertEq3Failed(AssertEq3Error {
                         span_lhs,
                         lhs_original,
                         lhs_converted,

+ 8 - 8
numbat/src/ffi/strings.rs

@@ -1,24 +1,24 @@
 use super::macros::*;
 use super::Args;
 use super::Result;
+use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::value::Value;
-use crate::RuntimeError;
 
-pub fn str_length(mut args: Args) -> Result<Value> {
+pub fn str_length(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let len = string_arg!(args).len();
     return_scalar!(len as f64)
 }
 
-pub fn lowercase(mut args: Args) -> Result<Value> {
+pub fn lowercase(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     return_string!(owned = string_arg!(args).to_lowercase())
 }
 
-pub fn uppercase(mut args: Args) -> Result<Value> {
+pub fn uppercase(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     return_string!(owned = string_arg!(args).to_uppercase())
 }
 
-pub fn str_slice(mut args: Args) -> Result<Value> {
+pub fn str_slice(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let start = quantity_arg!(args).unsafe_value().to_f64() as usize;
     let end = quantity_arg!(args).unsafe_value().to_f64() as usize;
     let input = string_arg!(args);
@@ -28,7 +28,7 @@ pub fn str_slice(mut args: Args) -> Result<Value> {
     return_string!(borrowed = output)
 }
 
-pub fn chr(mut args: Args) -> Result<Value> {
+pub fn chr(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let idx = quantity_arg!(args).unsafe_value().to_f64() as u32;
 
     let output = char::from_u32(idx).unwrap_or('�');
@@ -36,11 +36,11 @@ pub fn chr(mut args: Args) -> Result<Value> {
     return_string!(from = &output)
 }
 
-pub fn ord(mut args: Args) -> Result<Value> {
+pub fn ord(mut args: Args) -> Result<Value, Box<RuntimeErrorKind>> {
     let input = string_arg!(args);
 
     if input.is_empty() {
-        return Err(Box::new(RuntimeError::EmptyList));
+        return Err(Box::new(RuntimeErrorKind::EmptyList));
     }
 
     let output = input.chars().next().unwrap() as u32;

+ 20 - 5
numbat/src/interpreter/mod.rs

@@ -1,5 +1,7 @@
 pub(crate) mod assert_eq;
 
+use std::fmt;
+
 use crate::{
     dimension::DimensionRegistry,
     markup::Markup,
@@ -18,9 +20,22 @@ use thiserror::Error;
 
 pub use crate::value::Value;
 
+#[derive(Debug, Clone, Error)]
+pub struct RuntimeError {
+    pub kind: RuntimeErrorKind,
+    pub backtrace: Vec<Span>,
+}
+
+impl fmt::Display for RuntimeError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        // TODO: TAMO: display the backtrace
+        write!(f, "{}", self.kind)
+    }
+}
+
 #[derive(Debug, Clone, Error, PartialEq)]
 #[allow(clippy::large_enum_variant)]
-pub enum RuntimeError {
+pub enum RuntimeErrorKind {
     #[error("Division by zero")]
     DivisionByZero,
     #[error("Expected factorial argument to be a non-negative integer")]
@@ -244,9 +259,9 @@ mod tests {
     }
 
     #[track_caller]
-    fn assert_runtime_error(input: &str, err_expected: RuntimeError) {
+    fn assert_runtime_error(input: &str, err_expected: RuntimeErrorKind) {
         if let Err(err_actual) = get_interpreter_result(input) {
-            assert_eq!(*err_actual, err_expected);
+            assert_eq!(err_actual.kind, err_expected);
         } else {
             panic!();
         }
@@ -280,7 +295,7 @@ mod tests {
 
         assert_runtime_error(
             "1 meter > alternative_length_base_unit",
-            RuntimeError::QuantityError(QuantityError::IncompatibleUnits(
+            RuntimeErrorKind::QuantityError(QuantityError::IncompatibleUnits(
                 Unit::new_base(
                     CompactString::const_new("meter"),
                     CanonicalName::new("m", AcceptsPrefix::only_short()),
@@ -352,6 +367,6 @@ mod tests {
 
     #[test]
     fn division_by_zero_raises_runtime_error() {
-        assert_runtime_error("1/0", RuntimeError::DivisionByZero);
+        assert_runtime_error("1/0", RuntimeErrorKind::DivisionByZero);
     }
 }

+ 7 - 2
numbat/src/lib.rs

@@ -76,7 +76,7 @@ use typechecker::{TypeCheckError, TypeChecker};
 pub use diagnostic::Diagnostic;
 pub use interpreter::InterpreterResult;
 pub use interpreter::InterpreterSettings;
-pub use interpreter::RuntimeError;
+pub use interpreter::{RuntimeError, RuntimeErrorKind};
 pub use name_resolution::NameResolutionError;
 pub use parser::ParseError;
 pub use registry::BaseRepresentation;
@@ -162,6 +162,10 @@ impl Context {
         ExchangeRatesCache::use_test_rates();
     }
 
+    pub fn runtime_error(&self, kind: RuntimeErrorKind) -> RuntimeError {
+        self.interpreter.runtime_error(kind)
+    }
+
     pub fn variable_names(&self) -> impl Iterator<Item = CompactString> + '_ {
         self.prefix_transformer
             .variable_names
@@ -760,7 +764,8 @@ impl Context {
 
                             if erc.is_none() {
                                 return Err(Box::new(NumbatError::RuntimeError(
-                                    RuntimeError::CouldNotLoadExchangeRates,
+                                    self.interpreter
+                                        .runtime_error(RuntimeErrorKind::CouldNotLoadExchangeRates),
                                 )));
                             }
                         }

+ 4 - 4
numbat/src/list.rs

@@ -7,7 +7,7 @@
 
 use std::{collections::VecDeque, fmt, sync::Arc};
 
-use crate::{value::Value, RuntimeError};
+use crate::{interpreter::RuntimeErrorKind, value::Value, RuntimeError};
 
 /// Reference counted list / list view
 #[derive(Clone, Eq)]
@@ -76,9 +76,9 @@ impl<T> NumbatList<T> {
 
     /// Return the tail of the list without the first element.
     /// Return an error if the list is empty.
-    pub fn tail(&mut self) -> Result<(), Box<RuntimeError>> {
+    pub fn tail(&mut self) -> Result<(), Box<RuntimeErrorKind>> {
         if self.is_empty() {
-            return Err(Box::new(RuntimeError::EmptyList));
+            return Err(Box::new(RuntimeErrorKind::EmptyList));
         }
         if let Some(view) = &mut self.view {
             view.0 += 1;
@@ -236,7 +236,7 @@ mod test {
         assert!(list.is_empty());
         assert_eq!(alloc, Arc::as_ptr(&list.alloc));
 
-        assert_eq!(list.tail(), Err(Box::new(RuntimeError::EmptyList)));
+        assert_eq!(list.tail(), Err(Box::new(RuntimeErrorKind::EmptyList)));
     }
 
     #[test]

+ 5 - 5
numbat/src/session_history.rs

@@ -1,7 +1,7 @@
 use compact_str::CompactString;
 use std::{fs, io, path::Path};
 
-use crate::RuntimeError;
+use crate::{interpreter::RuntimeErrorKind, RuntimeError};
 
 pub type ParseEvaluationResult = Result<(), ()>;
 
@@ -35,8 +35,8 @@ impl SessionHistory {
         &self,
         mut w: impl io::Write,
         options: SessionHistoryOptions,
-        err_fn: impl Fn(io::Error) -> RuntimeError,
-    ) -> Result<(), Box<RuntimeError>> {
+        err_fn: impl Fn(io::Error) -> RuntimeErrorKind,
+    ) -> Result<(), Box<RuntimeErrorKind>> {
         let SessionHistoryOptions {
             include_err_lines,
             trim_lines,
@@ -62,9 +62,9 @@ impl SessionHistory {
         &self,
         dst: impl AsRef<Path>,
         options: SessionHistoryOptions,
-    ) -> Result<(), Box<RuntimeError>> {
+    ) -> Result<(), Box<RuntimeErrorKind>> {
         let dst = dst.as_ref();
-        let err_fn = |_: io::Error| RuntimeError::FileWrite(dst.to_owned());
+        let err_fn = |_: io::Error| RuntimeErrorKind::FileWrite(dst.to_owned());
 
         let f = fs::File::create(dst).map_err(err_fn)?;
         self.save_inner(f, options, err_fn)

+ 58 - 32
numbat/src/vm.rs

@@ -6,6 +6,7 @@ use compact_str::{CompactString, ToCompactString};
 use indexmap::IndexMap;
 use num_traits::ToPrimitive;
 
+use crate::interpreter::RuntimeErrorKind;
 use crate::list::NumbatList;
 use crate::span::Span;
 use crate::typed_ast::StructInfo;
@@ -345,6 +346,17 @@ impl Vm {
         self.debug = activate;
     }
 
+    pub(crate) fn runtime_error(&self, kind: RuntimeErrorKind) -> RuntimeError {
+        RuntimeError {
+            kind,
+            backtrace: self.backtrace(),
+        }
+    }
+
+    pub fn backtrace(&self) -> Vec<Span> {
+        todo!()
+    }
+
     // The following functions are helpers for the compilation process
 
     fn current_chunk_mut(&mut self) -> (&mut Vec<u8>, &mut Vec<Span>) {
@@ -663,7 +675,7 @@ impl Vm {
 
                     self.unit_registry
                         .add_derived_unit(unit_name, &base_unit_representation, metadata.clone())
-                        .map_err(RuntimeError::UnitRegistryError)?;
+                        .map_err(|e| self.runtime_error(RuntimeErrorKind::UnitRegistryError(e)))?;
 
                     self.constants[constant_idx as usize] = Constant::Unit(Unit::new_derived(
                         unit_name.clone(),
@@ -696,16 +708,19 @@ impl Vm {
                         Op::Add => &lhs + &rhs,
                         Op::Subtract => &lhs - &rhs,
                         Op::Multiply => Ok(lhs * rhs),
-                        Op::Divide => {
-                            Ok(lhs.checked_div(rhs).ok_or(RuntimeError::DivisionByZero)?)
-                        }
+                        Op::Divide => Ok(lhs
+                            .checked_div(rhs)
+                            .ok_or_else(|| self.runtime_error(RuntimeErrorKind::DivisionByZero))?),
                         Op::Power => lhs.power(rhs),
                         // If the user specifically converted the type of a unit, we should NOT simplify this value
                         // before any operations are applied to it
                         Op::ConvertTo => lhs.convert_to(rhs.unit()).map(Quantity::no_simplify),
                         _ => unreachable!(),
                     };
-                    self.push_quantity(result.map_err(RuntimeError::QuantityError)?);
+                    self.push_quantity(
+                        result
+                            .map_err(|e| self.runtime_error(RuntimeErrorKind::QuantityError(e)))?,
+                    );
                 }
                 op @ (Op::AddToDateTime | Op::SubFromDateTime) => {
                     let rhs = self.pop_quantity();
@@ -717,20 +732,20 @@ impl Vm {
 
                     let seconds_i64 = seconds_f64
                         .to_i64()
-                        .ok_or(RuntimeError::DurationOutOfRange)?;
+                        .ok_or_else(|| self.runtime_error(RuntimeErrorKind::DurationOutOfRange))?;
 
                     let span = jiff::Span::new()
                         .try_seconds(seconds_i64)
-                        .map_err(|_| RuntimeError::DurationOutOfRange)?
+                        .map_err(|_| self.runtime_error(RuntimeErrorKind::DurationOutOfRange))?
                         .nanoseconds((seconds_f64.fract() * 1_000_000_000f64).round() as i64);
 
                     self.push(Value::DateTime(match op {
-                        Op::AddToDateTime => lhs
-                            .checked_add(span)
-                            .map_err(|_| RuntimeError::DateTimeOutOfRange)?,
-                        Op::SubFromDateTime => lhs
-                            .checked_sub(span)
-                            .map_err(|_| RuntimeError::DateTimeOutOfRange)?,
+                        Op::AddToDateTime => lhs.checked_add(span).map_err(|_| {
+                            self.runtime_error(RuntimeErrorKind::DateTimeOutOfRange)
+                        })?,
+                        Op::SubFromDateTime => lhs.checked_sub(span).map_err(|_| {
+                            self.runtime_error(RuntimeErrorKind::DateTimeOutOfRange)
+                        })?,
                         _ => unreachable!(),
                     }));
                 }
@@ -741,10 +756,10 @@ impl Vm {
 
                     let duration = lhs
                         .since(&rhs)
-                        .map_err(|_| RuntimeError::DateTimeOutOfRange)?;
+                        .map_err(|_| self.runtime_error(RuntimeErrorKind::DateTimeOutOfRange))?;
                     let duration = duration
                         .total(jiff::Unit::Second)
-                        .map_err(|_| RuntimeError::DurationOutOfRange)?;
+                        .map_err(|_| self.runtime_error(RuntimeErrorKind::DurationOutOfRange))?;
 
                     let ret = Value::Quantity(Quantity::new(
                         Number::from_f64(duration),
@@ -762,11 +777,11 @@ impl Vm {
 
                     let result = match lhs.partial_cmp_preserve_nan(&rhs) {
                         QuantityOrdering::IncompatibleUnits => {
-                            return Err(Box::new(RuntimeError::QuantityError(
-                                QuantityError::IncompatibleUnits(
+                            return Err(Box::new(self.runtime_error(
+                                RuntimeErrorKind::QuantityError(QuantityError::IncompatibleUnits(
                                     lhs.unit().clone(),
                                     rhs.unit().clone(),
-                                ),
+                                )),
                             )))
                         }
                         QuantityOrdering::NanOperand => false,
@@ -823,9 +838,13 @@ impl Vm {
                     let order = self.read_u16();
 
                     if lhs < 0. {
-                        return Err(Box::new(RuntimeError::FactorialOfNegativeNumber));
+                        return Err(Box::new(
+                            self.runtime_error(RuntimeErrorKind::FactorialOfNegativeNumber),
+                        ));
                     } else if lhs.fract() != 0. {
-                        return Err(Box::new(RuntimeError::FactorialOfNonInteger));
+                        return Err(Box::new(
+                            self.runtime_error(RuntimeErrorKind::FactorialOfNonInteger),
+                        ));
                     }
 
                     self.push_quantity(Quantity::from_scalar(math::factorial(lhs, order)));
@@ -863,8 +882,8 @@ impl Vm {
 
                     match &self.ffi_callables[function_idx].callable {
                         Callable::Function(function) => {
-                            let result = (function)(args);
-                            self.push(result?);
+                            let result = (function)(args).map_err(|e| self.runtime_error(*e))?;
+                            self.push(result);
                         }
                         Callable::Procedure(procedure) => {
                             let span_idx = self.read_u16() as usize;
@@ -875,7 +894,7 @@ impl Vm {
                             match result {
                                 std::ops::ControlFlow::Continue(()) => {}
                                 std::ops::ControlFlow::Break(runtime_error) => {
-                                    return Err(Box::new(runtime_error));
+                                    return Err(Box::new(self.runtime_error(runtime_error)));
                                 }
                             }
                         }
@@ -909,8 +928,8 @@ impl Vm {
 
                             match &self.ffi_callables[function_idx].callable {
                                 Callable::Function(function) => {
-                                    let result = (function)(args);
-                                    self.push(result?);
+                                    let result = (function)(args).map_err(|e| self.runtime_error(*e))?;
+                                    self.push(result);
                                 }
                                 Callable::Procedure(..) => unreachable!("Foreign procedures can not be targeted by a function reference"),
                             }
@@ -920,8 +939,11 @@ impl Vm {
 
                             let dt = self.pop_datetime();
 
-                            let tz = jiff::tz::TimeZone::get(&tz_name)
-                                .map_err(|_| RuntimeError::UnknownTimezone(tz_name.to_string()))?;
+                            let tz = jiff::tz::TimeZone::get(&tz_name).map_err(|_| {
+                                self.runtime_error(RuntimeErrorKind::UnknownTimezone(
+                                    tz_name.to_string(),
+                                ))
+                            })?;
 
                             let dt = dt.with_time_zone(tz);
 
@@ -948,10 +970,12 @@ impl Vm {
                         Value::FormatSpecifiers(_) => unreachable!(),
                     };
 
-                    let map_strfmt_error_to_runtime_error = |err| match err {
-                        strfmt::FmtError::Invalid(s) => RuntimeError::InvalidFormatSpecifiers(s),
+                    let map_strfmt_error_to_runtime_error = |this: &Self, err| match err {
+                        strfmt::FmtError::Invalid(s) => {
+                            this.runtime_error(RuntimeErrorKind::InvalidFormatSpecifiers(s))
+                        }
                         strfmt::FmtError::TypeError(s) => {
-                            RuntimeError::InvalidTypeForFormatSpecifiers(s)
+                            this.runtime_error(RuntimeErrorKind::InvalidTypeForFormatSpecifiers(s))
                         }
                         strfmt::FmtError::KeyError(_) => unreachable!(),
                     };
@@ -971,7 +995,9 @@ impl Vm {
                                     let mut str =
                                         strfmt::strfmt(&format!("{{value{specifiers}}}"), &vars)
                                             .map(CompactString::from)
-                                            .map_err(map_strfmt_error_to_runtime_error)?;
+                                            .map_err(|e| {
+                                                map_strfmt_error_to_runtime_error(self, e)
+                                            })?;
 
                                     let unit_str = q.unit().to_compact_string();
 
@@ -988,7 +1014,7 @@ impl Vm {
 
                                     strfmt::strfmt(&format!("{{value{specifiers}}}"), &vars)
                                         .map(CompactString::from)
-                                        .map_err(map_strfmt_error_to_runtime_error)?
+                                        .map_err(|e| map_strfmt_error_to_runtime_error(self, e))?
                                 }
                             },
                             Value::FormatSpecifiers(None) => to_str(self.pop()),