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.
* 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).
* 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).

View File

@ -127,9 +127,13 @@ impl Engine {
}
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")]
if self.is_debugger_registered() {
@ -139,7 +143,7 @@ impl Engine {
self.run_debugger(global, caches, scope, &mut this, node)?;
}
Ok(())
result
}
}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
use super::call::FnCallArgs;
use crate::ast::ScriptFnDef;
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;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -33,32 +33,6 @@ impl Engine {
rewind_scope: bool,
pos: Position,
) -> 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());
self.track_operation(global, pos)?;
@ -97,14 +71,13 @@ impl Engine {
// Push a new call stack frame
#[cfg(feature = "debugging")]
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();
global.debugger_mut().push_call_stack_frame(
fn_def.name.clone(),
scope.iter().skip(orig_scope_len).map(|(.., v)| v).collect(),
source,
pos,
);
global
.debugger_mut()
.push_call_stack_frame(fn_name, args, source, pos);
}
// Merge in encapsulated environment, if any
@ -137,27 +110,32 @@ impl Engine {
}
// 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)
.or_else(|err| match *err {
// Convert return statement to return value
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
mut err if err.is_system_exception() => {
err.set_position(pos);
Err(err.into())
}
// 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")]

View File

@ -135,22 +135,20 @@ impl fmt::Display for EvalAltResult {
#[cfg(not(feature = "no_function"))]
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() {
write!(f, " @ '{src}'")?;
}
}
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() {
write!(f, " @ '{src}'")?;
}
}
Self::ErrorInModule(s, err, ..) if s.is_empty() => {
write!(f, "Error in module > {err}")?
}
Self::ErrorInModule(s, err, ..) => write!(f, "Error in module '{s}' > {err}")?,
Self::ErrorInModule(s, err, ..) if s.is_empty() => write!(f, "{err}\n| in module")?,
Self::ErrorInModule(s, err, ..) => write!(f, "{err}\n| in module '{s}'")?,
Self::ErrorVariableExists(s, ..) => write!(f, "Variable already defined: {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::ErrorFunctionNotFound(s, ..) => write!(f, "Function not found: {s}")?,
Self::ErrorModuleNotFound(s, ..) => write!(f, "Module not found: {s}")?,
Self::ErrorDataRace(s, ..) => {
write!(f, "Data race detected when accessing variable: {s}")?
}
Self::ErrorDataRace(s, ..) => write!(f, "Data race detected on variable '{s}'")?,
Self::ErrorDotExpr(s, ..) if s.is_empty() => f.write_str("Malformed dot expression")?,
Self::ErrorDotExpr(s, ..) => f.write_str(s)?,
Self::ErrorIndexingType(s, ..) => write!(f, "Indexer unavailable: {s}")?,
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::ErrorTooManyModules(..) => f.write_str("Too many modules imported")?,
Self::ErrorStackOverflow(..) => f.write_str("Stack overflow")?,
@ -188,31 +184,25 @@ impl fmt::Display for EvalAltResult {
if s.starts_with(crate::engine::FN_GET) =>
{
let prop = &s[crate::engine::FN_GET.len()..];
write!(
f,
"Property {prop} is not pure and cannot be accessed on a constant"
)?
write!(f, "Non-pure property {prop} cannot be accessed on constant")?
}
#[cfg(not(feature = "no_object"))]
Self::ErrorNonPureMethodCallOnConstant(s, ..)
if s.starts_with(crate::engine::FN_SET) =>
{
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"))]
Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_GET => {
write!(
f,
"Indexer is not pure and cannot be accessed on a constant"
)?
write!(f, "Non-pure indexer cannot be accessed on constant")?
}
#[cfg(not(feature = "no_index"))]
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, ..) => {
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}")?,
@ -230,8 +220,8 @@ impl fmt::Display for EvalAltResult {
Self::ErrorArithmetic(s, ..) if s.is_empty() => f.write_str("Arithmetic error")?,
Self::ErrorArithmetic(s, ..) => f.write_str(s)?,
Self::LoopBreak(true, ..) => f.write_str("'break' must be inside a loop")?,
Self::LoopBreak(false, ..) => f.write_str("'continue' 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 within a loop")?,
Self::Return(..) => f.write_str("NOT AN ERROR - function returns value")?,
@ -261,7 +251,7 @@ impl fmt::Display for EvalAltResult {
f,
"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(" "))?,
}