Pārlūkot izejas kodu

display the backtrace in case of runtime error

Tamo 1 nedēļu atpakaļ
vecāks
revīzija
41960364ed

+ 23 - 7
numbat-cli/src/main.rs

@@ -11,12 +11,12 @@ use highlighter::NumbatHighlighter;
 
 use itertools::Itertools;
 use numbat::command::{CommandControlFlow, CommandRunner};
-use numbat::diagnostic::ErrorDiagnostic;
+use numbat::diagnostic::{ErrorDiagnostic, ResolverDiagnostic};
 use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystemImporter};
 use numbat::pretty_print::PrettyPrint;
 use numbat::resolver::CodeSource;
 use numbat::session_history::{ParseEvaluationResult, SessionHistory};
-use numbat::{markup as m, RuntimeError, RuntimeErrorKind};
+use numbat::{markup as m, RuntimeErrorKind};
 use numbat::{Context, NumbatError};
 use numbat::{InterpreterSettings, NameResolutionError};
 
@@ -382,9 +382,15 @@ impl Cli {
                     let mut ctx = self.context.lock().unwrap();
 
                     if interactive && rl.append_history(history_path).is_err() {
-                        ctx.print_diagnostic(ctx.runtime_error(RuntimeErrorKind::HistoryWrite(
-                            history_path.to_owned(),
-                        )));
+                        ctx.print_diagnostic(
+                            ResolverDiagnostic {
+                                resolver: ctx.resolver(),
+                                error: &ctx.runtime_error(RuntimeErrorKind::HistoryWrite(
+                                    history_path.to_owned(),
+                                )),
+                            },
+                            colored::control::SHOULD_COLORIZE.should_colorize(),
+                        );
                     }
 
                     match cmd_runner.try_run_command(&line, &mut ctx, rl) {
@@ -395,7 +401,10 @@ impl Cli {
                         },
                         Err(err) => {
                             ctx.print_diagnostic(
-                                *err,
+                                ResolverDiagnostic {
+                                    resolver: ctx.resolver(),
+                                    error: &*err,
+                                },
                                 colored::control::SHOULD_COLORIZE.should_colorize(),
                             );
                             continue;
@@ -529,7 +538,14 @@ impl Cli {
                 execution_mode.exit_status_in_case_of_error()
             }
             Err(NumbatError::RuntimeError(e)) => {
-                self.print_diagnostic(e);
+                let ctx = self.context.lock().unwrap();
+                ctx.print_diagnostic(
+                    ResolverDiagnostic {
+                        resolver: ctx.resolver(),
+                        error: &e,
+                    },
+                    colored::control::SHOULD_COLORIZE.should_colorize(),
+                );
                 execution_mode.exit_status_in_case_of_error()
             }
         };

+ 8 - 4
numbat/src/command.rs

@@ -3,7 +3,7 @@ use std::str::{FromStr, SplitWhitespace};
 use compact_str::ToCompactString;
 
 use crate::{
-    diagnostic::ErrorDiagnostic,
+    diagnostic::{ErrorDiagnostic, ResolverDiagnostic},
     help::help_markup,
     interpreter::RuntimeErrorKind,
     markup::{self as m, Markup},
@@ -82,11 +82,15 @@ impl From<RuntimeError> for CommandError {
     }
 }
 
-impl ErrorDiagnostic for CommandError {
+impl ErrorDiagnostic for ResolverDiagnostic<'_, CommandError> {
     fn diagnostics(&self) -> Vec<crate::Diagnostic> {
-        match self {
+        match self.error {
             CommandError::Parse(parse_error) => parse_error.diagnostics(),
-            CommandError::Runtime(runtime_error) => runtime_error.diagnostics(),
+            CommandError::Runtime(runtime_error) => ResolverDiagnostic {
+                resolver: self.resolver,
+                error: runtime_error,
+            }
+            .diagnostics(),
         }
     }
 }

+ 85 - 29
numbat/src/diagnostic.rs

@@ -4,7 +4,8 @@ use crate::{
     interpreter::{RuntimeError, RuntimeErrorKind},
     parser::ParseError,
     pretty_print::PrettyPrint,
-    resolver::ResolverError,
+    resolver::{Resolver, ResolverError},
+    span::Span,
     typechecker::{IncompatibleDimensionsError, TypeCheckError},
     NameResolutionError,
 };
@@ -490,18 +491,28 @@ impl ErrorDiagnostic for TypeCheckError {
     }
 }
 
-impl ErrorDiagnostic for RuntimeError {
+/// Little helper to quickly implement diagnostic for types that requires the resolver
+/// in order to emit a diagnostic.
+pub struct ResolverDiagnostic<'a, E> {
+    pub resolver: &'a Resolver,
+    pub error: &'a E,
+}
+
+impl ErrorDiagnostic for ResolverDiagnostic<'_, RuntimeError> {
     fn diagnostics(&self) -> Vec<Diagnostic> {
-        let inner = format!("{self:#}");
+        let mut diag = Vec::new();
 
-        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")])],
-            RuntimeErrorKind::AssertEq2Failed(assert_eq2_error) => {
-                vec![Diagnostic::error()
+        let inner = format!("{:#}", self.error.kind);
+        match &self.error.kind {
+            RuntimeErrorKind::AssertFailed(span) => diag.push(
+                Diagnostic::error()
+                    .with_message("assertion failed")
+                    .with_labels(vec![span
+                        .diagnostic_label(LabelStyle::Primary)
+                        .with_message("assertion failed")]),
+            ),
+            RuntimeErrorKind::AssertEq2Failed(assert_eq2_error) => diag.push(
+                Diagnostic::error()
                     .with_message("Assertion failed")
                     .with_labels(vec![
                         assert_eq2_error
@@ -513,28 +524,73 @@ impl ErrorDiagnostic for RuntimeError {
                             .diagnostic_label(LabelStyle::Primary)
                             .with_message(format!("{}", assert_eq2_error.rhs)),
                     ])
-                    .with_notes(vec![inner])]
-            }
+                    .with_notes(vec![inner]),
+            ),
             RuntimeErrorKind::AssertEq3Failed(assert_eq3_error) => {
                 let (lhs, rhs) = assert_eq3_error.fmt_comparands();
 
-                vec![Diagnostic::error()
-                    .with_message("Assertion failed")
-                    .with_labels(vec![
-                        assert_eq3_error
-                            .span_lhs
-                            .diagnostic_label(LabelStyle::Secondary)
-                            .with_message(lhs),
-                        assert_eq3_error
-                            .span_rhs
-                            .diagnostic_label(LabelStyle::Primary)
-                            .with_message(rhs),
-                    ])
-                    .with_notes(vec![format!("{self:#}")])]
+                diag.push(
+                    Diagnostic::error()
+                        .with_message("Assertion failed")
+                        .with_labels(vec![
+                            assert_eq3_error
+                                .span_lhs
+                                .diagnostic_label(LabelStyle::Secondary)
+                                .with_message(lhs),
+                            assert_eq3_error
+                                .span_rhs
+                                .diagnostic_label(LabelStyle::Primary)
+                                .with_message(rhs),
+                        ])
+                        .with_notes(vec![inner]),
+                )
             }
-            _ => vec![Diagnostic::error()
-                .with_message("runtime error")
-                .with_notes(vec![inner])],
+            _ => diag.push(
+                Diagnostic::error()
+                    .with_message("runtime error")
+                    .with_notes(vec![inner])
+                    .with_labels_iter(
+                        self.error
+                            .backtrace
+                            .iter()
+                            .take(1)
+                            .map(|span| span.1.diagnostic_label(LabelStyle::Primary)),
+                    ),
+            ),
+        }
+
+        diag.push(
+            Diagnostic::help()
+                .with_message("Backtrace:")
+                .with_notes_iter(self.error.backtrace.iter().enumerate().map(
+                    |(i, (fn_name, span))| {
+                        let file_name = self.resolver.files.get(span.code_source_id).unwrap();
+                        let (line, col) = position(*span, file_name.source());
+
+                        format!("{i}: in {fn_name} - {}:{}:{}", file_name.name(), line, col)
+                    },
+                )),
+        );
+
+        diag
+    }
+}
+
+/// Return the position in terms of line number and column number
+/// of the span starting position in the source.
+fn position(span: Span, source: &str) -> (usize, usize) {
+    let mut line = 1;
+    let mut last_line_pos = 0;
+    let start = span.start.as_usize();
+
+    for (i, b) in source.bytes().enumerate() {
+        if b == b'\n' {
+            line += 1;
+            last_line_pos = i;
+        }
+        if i == start {
+            break;
         }
     }
+    (line, start - last_line_pos)
 }

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

@@ -2,7 +2,7 @@ use std::collections::HashMap;
 use std::sync::OnceLock;
 
 use super::{macros::*, Args};
-use crate::{interpreter::RuntimeErrorKind, quantity::Quantity, value::Value, RuntimeError};
+use crate::{interpreter::RuntimeErrorKind, quantity::Quantity, value::Value};
 
 use super::{Callable, ForeignFunction, Result};
 

+ 0 - 1
numbat/src/ffi/lookup.rs

@@ -7,7 +7,6 @@ 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, Box<RuntimeErrorKind>> {
     use crate::span::{ByteIndex, Span};

+ 0 - 1
numbat/src/ffi/procedures.rs

@@ -14,7 +14,6 @@ use crate::{
     span::Span,
     value::Value,
     vm::ExecutionContext,
-    RuntimeError,
 };
 
 use super::{Args, Callable, ForeignFunction};

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

@@ -1,6 +1,6 @@
 pub(crate) mod assert_eq;
 
-use std::fmt;
+use core::fmt;
 
 use crate::{
     dimension::DimensionRegistry,
@@ -20,15 +20,14 @@ use thiserror::Error;
 
 pub use crate::value::Value;
 
-#[derive(Debug, Clone, Error)]
+#[derive(Debug, Clone)]
 pub struct RuntimeError {
     pub kind: RuntimeErrorKind,
-    pub backtrace: Vec<Span>,
+    pub backtrace: Vec<(CompactString, Span)>,
 }
 
 impl fmt::Display for RuntimeError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        // TODO: TAMO: display the backtrace
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", self.kind)
     }
 }

+ 1 - 1
numbat/src/list.rs

@@ -7,7 +7,7 @@
 
 use std::{collections::VecDeque, fmt, sync::Arc};
 
-use crate::{interpreter::RuntimeErrorKind, value::Value, RuntimeError};
+use crate::{interpreter::RuntimeErrorKind, value::Value};
 
 /// Reference counted list / list view
 #[derive(Clone, Eq)]

+ 1 - 1
numbat/src/session_history.rs

@@ -1,7 +1,7 @@
 use compact_str::CompactString;
 use std::{fs, io, path::Path};
 
-use crate::{interpreter::RuntimeErrorKind, RuntimeError};
+use crate::interpreter::RuntimeErrorKind;
 
 pub type ParseEvaluationResult = Result<(), ()>;
 

+ 13 - 2
numbat/src/vm.rs

@@ -353,8 +353,19 @@ impl Vm {
         }
     }
 
-    pub fn backtrace(&self) -> Vec<Span> {
-        todo!()
+    /// Return a list of function name + the span which triggered the error.
+    /// The deepest elements are returned first.
+    pub fn backtrace(&self) -> Vec<(CompactString, Span)> {
+        self.frames
+            .iter()
+            .rev()
+            .map(|cf| {
+                (
+                    self.bytecode[cf.function_idx].0.clone(),
+                    self.bytecode[cf.function_idx].2[cf.ip.saturating_sub(1)],
+                )
+            })
+            .collect()
     }
 
     // The following functions are helpers for the compilation process