Refine error display.

This commit is contained in:
Stephen Chung 2022-11-27 18:00:02 +08:00
parent 549193f49b
commit fcc7589ffc
7 changed files with 82 additions and 93 deletions

View File

@ -33,7 +33,7 @@ Standard features
----------------- -----------------
* Simple language similar to JavaScript+Rust with [dynamic](https://rhai.rs/book/language/dynamic.html) typing. * Simple language similar to JavaScript+Rust with [dynamic](https://rhai.rs/book/language/dynamic.html) typing.
* Fairly efficient evaluation (1 million iterations in 0.15 sec on a single-core 2.6 GHz Linux VM). * Fairly efficient evaluation (1 million iterations in 0.14 sec on a single-core 2.6 GHz Linux VM).
* Tight integration with native Rust [functions](https://rhai.rs/book/rust/functions.html) and [types](https://rhai.rs/book/rust/custom-types.html), including [getters/setters](https://rhai.rs/book/rust/getters-setters.html), [methods](https://rhai.rs/book/rust/methods.html) and [indexers](https://rhai.rs/book/rust/indexers.html). * Tight integration with native Rust [functions](https://rhai.rs/book/rust/functions.html) and [types](https://rhai.rs/book/rust/custom-types.html), including [getters/setters](https://rhai.rs/book/rust/getters-setters.html), [methods](https://rhai.rs/book/rust/methods.html) and [indexers](https://rhai.rs/book/rust/indexers.html).
* Freely pass Rust values into a script as [variables](https://rhai.rs/book/language/variables.html)/[constants](https://rhai.rs/book/language/constants.html) via an external [`Scope`](https://rhai.rs/book/engine/scope.html) - all clonable Rust types are supported; no need to implement any special trait. Or tap directly into the [variable resolution process](https://rhai.rs/book/engine/var.html). * Freely pass Rust values into a script as [variables](https://rhai.rs/book/language/variables.html)/[constants](https://rhai.rs/book/language/constants.html) via an external [`Scope`](https://rhai.rs/book/engine/scope.html) - all clonable Rust types are supported; no need to implement any special trait. Or tap directly into the [variable resolution process](https://rhai.rs/book/engine/var.html).
* Built-in support for most common [data types](https://rhai.rs/book/language/values-and-types.html) including booleans, [integers](https://rhai.rs/book/language/numbers.html), [floating-point numbers](https://rhai.rs/book/language/numbers.html) (including [`Decimal`](https://crates.io/crates/rust_decimal)), [strings](https://rhai.rs/book/language/strings-chars.html), [Unicode characters](https://rhai.rs/book/language/strings-chars.html), [arrays](https://rhai.rs/book/language/arrays.html) (including packed [byte arrays](https://rhai.rs/book/language/blobs.html)) and [object maps](https://rhai.rs/book/language/object-maps.html). * Built-in support for most common [data types](https://rhai.rs/book/language/values-and-types.html) including booleans, [integers](https://rhai.rs/book/language/numbers.html), [floating-point numbers](https://rhai.rs/book/language/numbers.html) (including [`Decimal`](https://crates.io/crates/rust_decimal)), [strings](https://rhai.rs/book/language/strings-chars.html), [Unicode characters](https://rhai.rs/book/language/strings-chars.html), [arrays](https://rhai.rs/book/language/arrays.html) (including packed [byte arrays](https://rhai.rs/book/language/blobs.html)) and [object maps](https://rhai.rs/book/language/object-maps.html).

View File

@ -127,9 +127,13 @@ impl Engine {
} }
let statements = ast.statements(); let statements = ast.statements();
if !statements.is_empty() {
self.eval_global_statements(global, caches, scope, statements)?; let result = if !statements.is_empty() {
} self.eval_global_statements(global, caches, scope, statements)
.map(|_| ())
} else {
Ok(())
};
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.is_debugger_registered() { if self.is_debugger_registered() {
@ -139,7 +143,7 @@ impl Engine {
self.run_debugger(global, caches, scope, &mut this, node)?; self.run_debugger(global, caches, scope, &mut this, node)?;
} }
Ok(()) result
} }
} }

View File

@ -100,19 +100,21 @@ fn print_error(input: &str, mut err: EvalAltResult) {
// Print error position // Print error position
if pos.is_none() { if pos.is_none() {
// No position // No position
println!("{err}"); println!("\x1b[31m{err}\x1b[39m");
} else { } else {
// Specific position - print line text // Specific position - print line text
println!("{line_no}{}", lines[pos.line().unwrap() - 1]); println!("{line_no}{}", lines[pos.line().unwrap() - 1]);
for (i, err_line) in err.to_string().split('\n').enumerate() {
// Display position marker // Display position marker
println!( println!(
"{0:>1$} {err}", "\x1b[31m{0:>1$}{err_line}\x1b[39m",
"^", if i > 0 { " " } else { "^ " },
line_no.len() + pos.position().unwrap(), line_no.len() + pos.position().unwrap() + 1,
); );
} }
} }
}
/// Print debug help. /// Print debug help.
fn print_debug_help() { fn print_debug_help() {
@ -237,7 +239,13 @@ fn debug_callback(
) -> Result<DebuggerCommand, Box<EvalAltResult>> { ) -> Result<DebuggerCommand, Box<EvalAltResult>> {
// Check event // Check event
match event { match event {
DebuggerEvent::Start if source.is_some() => {
println!("\x1b[32m! Script '{}' start\x1b[39m", source.unwrap())
}
DebuggerEvent::Start => println!("\x1b[32m! Script start\x1b[39m"), DebuggerEvent::Start => println!("\x1b[32m! Script start\x1b[39m"),
DebuggerEvent::End if source.is_some() => {
println!("\x1b[31m! Script '{}' end\x1b[39m", source.unwrap())
}
DebuggerEvent::End => println!("\x1b[31m! Script end\x1b[39m"), DebuggerEvent::End => println!("\x1b[31m! Script end\x1b[39m"),
DebuggerEvent::Step => (), DebuggerEvent::Step => (),
DebuggerEvent::BreakPoint(n) => { DebuggerEvent::BreakPoint(n) => {
@ -572,7 +580,7 @@ fn debug_callback(
break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into());
} }
["run" | "r"] => { ["run" | "r"] => {
println!("Restarting script..."); println!("Terminating current run...");
break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into());
} }
_ => eprintln!( _ => eprintln!(
@ -630,10 +638,13 @@ fn main() {
while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &ast) { while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &ast) {
match *err { match *err {
// Loop back to restart // Loop back to restart
EvalAltResult::ErrorTerminated(..) => (), EvalAltResult::ErrorTerminated(..) => {
println!("Restarting script...");
}
// Break evaluation // Break evaluation
_ => { _ => {
print_error(&script, *err); print_error(&script, *err);
println!();
break; break;
} }
} }

View File

@ -31,14 +31,16 @@ fn print_error(input: &str, mut err: EvalAltResult) {
// Specific position - print line text // Specific position - print line text
println!("{line_no}{}", lines[pos.line().unwrap() - 1]); println!("{line_no}{}", lines[pos.line().unwrap() - 1]);
for (i, err_line) in err.to_string().split('\n').enumerate() {
// Display position marker // Display position marker
println!( println!(
"{0:>1$} {err}", "{0:>1$}{err_line}",
"^", if i > 0 { " " } else { "^ " },
line_no.len() + pos.position().unwrap(), line_no.len() + pos.position().unwrap() + 1,
); );
} }
} }
}
/// Print help text. /// Print help text.
fn print_help() { fn print_help() {

View File

@ -8,11 +8,15 @@ fn eprint_error(input: &str, mut err: EvalAltResult) {
let line_no = format!("{line}: "); let line_no = format!("{line}: ");
eprintln!("{line_no}{}", lines[line - 1]); eprintln!("{line_no}{}", lines[line - 1]);
eprintln!(
"{:>1$} {err_msg}", for (i, err_line) in err_msg.to_string().split('\n').enumerate() {
"^", // Display position marker
line_no.len() + pos.position().unwrap(), println!(
"{0:>1$}{err_line}",
if i > 0 { " " } else { "^ " },
line_no.len() + pos.position().unwrap() + 1,
); );
}
eprintln!(); eprintln!();
} }

View File

@ -4,7 +4,7 @@
use super::call::FnCallArgs; use super::call::FnCallArgs;
use crate::ast::ScriptFnDef; use crate::ast::ScriptFnDef;
use crate::eval::{Caches, GlobalRuntimeState}; use crate::eval::{Caches, GlobalRuntimeState};
use crate::{Dynamic, Engine, Position, RhaiError, RhaiResult, Scope, ERR}; use crate::{Dynamic, Engine, Position, RhaiResult, Scope, ERR};
use std::mem; use std::mem;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -33,32 +33,6 @@ impl Engine {
rewind_scope: bool, rewind_scope: bool,
pos: Position, pos: Position,
) -> RhaiResult { ) -> RhaiResult {
#[cold]
#[inline(never)]
fn make_error(
name: String,
_fn_def: &ScriptFnDef,
global: &GlobalRuntimeState,
err: RhaiError,
pos: Position,
) -> RhaiResult {
#[cfg(not(feature = "no_module"))]
let source = _fn_def
.environ
.as_ref()
.and_then(|environ| environ.lib.id().map(str::to_string));
#[cfg(feature = "no_module")]
let source = None;
Err(ERR::ErrorInFunctionCall(
name,
source.unwrap_or_else(|| global.source().unwrap_or("").to_string()),
err,
pos,
)
.into())
}
assert!(fn_def.params.len() == args.len()); assert!(fn_def.params.len() == args.len());
self.track_operation(global, pos)?; self.track_operation(global, pos)?;
@ -97,14 +71,13 @@ impl Engine {
// Push a new call stack frame // Push a new call stack frame
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.is_debugger_registered() { if self.is_debugger_registered() {
let fn_name = fn_def.name.clone();
let args = scope.iter().skip(orig_scope_len).map(|(.., v)| v).collect();
let source = global.source.clone(); let source = global.source.clone();
global.debugger_mut().push_call_stack_frame( global
fn_def.name.clone(), .debugger_mut()
scope.iter().skip(orig_scope_len).map(|(.., v)| v).collect(), .push_call_stack_frame(fn_name, args, source, pos);
source,
pos,
);
} }
// Merge in encapsulated environment, if any // Merge in encapsulated environment, if any
@ -137,27 +110,32 @@ impl Engine {
} }
// Evaluate the function // Evaluate the function
let mut _result = self let mut _result: RhaiResult = self
.eval_stmt_block(global, caches, scope, this_ptr, &fn_def.body, rewind_scope) .eval_stmt_block(global, caches, scope, this_ptr, &fn_def.body, rewind_scope)
.or_else(|err| match *err { .or_else(|err| match *err {
// Convert return statement to return value // Convert return statement to return value
ERR::Return(x, ..) => Ok(x), ERR::Return(x, ..) => Ok(x),
// Error in sub function call
ERR::ErrorInFunctionCall(name, src, err, ..) => {
let fn_name = if src.is_empty() {
format!("{name} < {}", fn_def.name)
} else {
format!("{name} @ '{src}' < {}", fn_def.name)
};
make_error(fn_name, fn_def, global, err, pos)
}
// System errors are passed straight-through // System errors are passed straight-through
mut err if err.is_system_exception() => { mut err if err.is_system_exception() => {
err.set_position(pos); err.set_position(pos);
Err(err.into()) Err(err.into())
} }
// Other errors are wrapped in `ErrorInFunctionCall` // Other errors are wrapped in `ErrorInFunctionCall`
_ => make_error(fn_def.name.to_string(), fn_def, global, err, pos), _ => Err(ERR::ErrorInFunctionCall(
fn_def.name.to_string(),
#[cfg(not(feature = "no_module"))]
fn_def
.environ
.as_deref()
.and_then(|environ| environ.lib.id())
.unwrap_or_else(|| global.source().unwrap_or(""))
.to_string(),
#[cfg(feature = "no_module")]
global.source().unwrap_or("").to_string(),
err,
pos,
)
.into()),
}); });
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]

View File

@ -135,22 +135,20 @@ impl fmt::Display for EvalAltResult {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
Self::ErrorInFunctionCall(s, src, err, ..) if crate::parser::is_anonymous_fn(s) => { Self::ErrorInFunctionCall(s, src, err, ..) if crate::parser::is_anonymous_fn(s) => {
write!(f, "{err} in call to closure")?; write!(f, "{err}\n| in closure call")?;
if !src.is_empty() { if !src.is_empty() {
write!(f, " @ '{src}'")?; write!(f, " @ '{src}'")?;
} }
} }
Self::ErrorInFunctionCall(s, src, err, ..) => { Self::ErrorInFunctionCall(s, src, err, ..) => {
write!(f, "{err} in call to function {s}")?; write!(f, "{err}\n| in call to function '{s}'")?;
if !src.is_empty() { if !src.is_empty() {
write!(f, " @ '{src}'")?; write!(f, " @ '{src}'")?;
} }
} }
Self::ErrorInModule(s, err, ..) if s.is_empty() => { Self::ErrorInModule(s, err, ..) if s.is_empty() => write!(f, "{err}\n| in module")?,
write!(f, "Error in module > {err}")? Self::ErrorInModule(s, err, ..) => write!(f, "{err}\n| in module '{s}'")?,
}
Self::ErrorInModule(s, err, ..) => write!(f, "Error in module '{s}' > {err}")?,
Self::ErrorVariableExists(s, ..) => write!(f, "Variable already defined: {s}")?, Self::ErrorVariableExists(s, ..) => write!(f, "Variable already defined: {s}")?,
Self::ErrorForbiddenVariable(s, ..) => write!(f, "Forbidden variable name: {s}")?, Self::ErrorForbiddenVariable(s, ..) => write!(f, "Forbidden variable name: {s}")?,
@ -159,16 +157,14 @@ impl fmt::Display for EvalAltResult {
Self::ErrorIndexNotFound(s, ..) => write!(f, "Invalid index: {s}")?, Self::ErrorIndexNotFound(s, ..) => write!(f, "Invalid index: {s}")?,
Self::ErrorFunctionNotFound(s, ..) => write!(f, "Function not found: {s}")?, Self::ErrorFunctionNotFound(s, ..) => write!(f, "Function not found: {s}")?,
Self::ErrorModuleNotFound(s, ..) => write!(f, "Module not found: {s}")?, Self::ErrorModuleNotFound(s, ..) => write!(f, "Module not found: {s}")?,
Self::ErrorDataRace(s, ..) => { Self::ErrorDataRace(s, ..) => write!(f, "Data race detected on variable '{s}'")?,
write!(f, "Data race detected when accessing variable: {s}")?
}
Self::ErrorDotExpr(s, ..) if s.is_empty() => f.write_str("Malformed dot expression")?, Self::ErrorDotExpr(s, ..) if s.is_empty() => f.write_str("Malformed dot expression")?,
Self::ErrorDotExpr(s, ..) => f.write_str(s)?, Self::ErrorDotExpr(s, ..) => f.write_str(s)?,
Self::ErrorIndexingType(s, ..) => write!(f, "Indexer unavailable: {s}")?, Self::ErrorIndexingType(s, ..) => write!(f, "Indexer unavailable: {s}")?,
Self::ErrorUnboundThis(..) => f.write_str("'this' not bound")?, Self::ErrorUnboundThis(..) => f.write_str("'this' not bound")?,
Self::ErrorFor(..) => f.write_str("For loop expects an iterable type")?, Self::ErrorFor(..) => f.write_str("For loop expects iterable type")?,
Self::ErrorTooManyOperations(..) => f.write_str("Too many operations")?, Self::ErrorTooManyOperations(..) => f.write_str("Too many operations")?,
Self::ErrorTooManyModules(..) => f.write_str("Too many modules imported")?, Self::ErrorTooManyModules(..) => f.write_str("Too many modules imported")?,
Self::ErrorStackOverflow(..) => f.write_str("Stack overflow")?, Self::ErrorStackOverflow(..) => f.write_str("Stack overflow")?,
@ -188,31 +184,25 @@ impl fmt::Display for EvalAltResult {
if s.starts_with(crate::engine::FN_GET) => if s.starts_with(crate::engine::FN_GET) =>
{ {
let prop = &s[crate::engine::FN_GET.len()..]; let prop = &s[crate::engine::FN_GET.len()..];
write!( write!(f, "Non-pure property {prop} cannot be accessed on constant")?
f,
"Property {prop} is not pure and cannot be accessed on a constant"
)?
} }
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
Self::ErrorNonPureMethodCallOnConstant(s, ..) Self::ErrorNonPureMethodCallOnConstant(s, ..)
if s.starts_with(crate::engine::FN_SET) => if s.starts_with(crate::engine::FN_SET) =>
{ {
let prop = &s[crate::engine::FN_SET.len()..]; let prop = &s[crate::engine::FN_SET.len()..];
write!(f, "Cannot modify property '{prop}' of a constant")? write!(f, "Cannot modify property '{prop}' of constant")?
} }
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_GET => { Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_GET => {
write!( write!(f, "Non-pure indexer cannot be accessed on constant")?
f,
"Indexer is not pure and cannot be accessed on a constant"
)?
} }
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_SET => { Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_SET => {
write!(f, "Cannot assign to the indexer of a constant")? write!(f, "Cannot assign to indexer of constant")?
} }
Self::ErrorNonPureMethodCallOnConstant(s, ..) => { Self::ErrorNonPureMethodCallOnConstant(s, ..) => {
write!(f, "Non-pure method '{s}' cannot be called on a constant")? write!(f, "Non-pure method '{s}' cannot be called on constant")?
} }
Self::ErrorAssignmentToConstant(s, ..) => write!(f, "Cannot modify constant {s}")?, Self::ErrorAssignmentToConstant(s, ..) => write!(f, "Cannot modify constant {s}")?,
@ -230,8 +220,8 @@ impl fmt::Display for EvalAltResult {
Self::ErrorArithmetic(s, ..) if s.is_empty() => f.write_str("Arithmetic error")?, Self::ErrorArithmetic(s, ..) if s.is_empty() => f.write_str("Arithmetic error")?,
Self::ErrorArithmetic(s, ..) => f.write_str(s)?, Self::ErrorArithmetic(s, ..) => f.write_str(s)?,
Self::LoopBreak(true, ..) => f.write_str("'break' must be inside a loop")?, Self::LoopBreak(true, ..) => f.write_str("'break' must be within a loop")?,
Self::LoopBreak(false, ..) => f.write_str("'continue' must be inside a loop")?, Self::LoopBreak(false, ..) => f.write_str("'continue' must be within a loop")?,
Self::Return(..) => f.write_str("NOT AN ERROR - function returns value")?, Self::Return(..) => f.write_str("NOT AN ERROR - function returns value")?,
@ -261,7 +251,7 @@ impl fmt::Display for EvalAltResult {
f, f,
"Bit-field index {index} out of bounds: only {max} bits in bit-field", "Bit-field index {index} out of bounds: only {max} bits in bit-field",
)?, )?,
Self::ErrorDataTooLarge(typ, ..) => write!(f, "{typ} exceeds maximum limit")?, Self::ErrorDataTooLarge(typ, ..) => write!(f, "{typ} too large")?,
Self::ErrorCustomSyntax(s, tokens, ..) => write!(f, "{s}: {}", tokens.join(" "))?, Self::ErrorCustomSyntax(s, tokens, ..) => write!(f, "{s}: {}", tokens.join(" "))?,
} }