use std::fmt;
pub type LuaResult<T> = Result<T, LuaError>;
#[derive(Debug)]
pub enum LuaError {
Syntax(SyntaxError),
Runtime(RuntimeError),
Memory,
ErrorHandler,
Io(std::io::Error),
Yield(u32),
}
#[derive(Debug)]
pub struct SyntaxError {
pub message: String,
pub source: String,
pub line: u32,
pub raw_message: Option<Vec<u8>>,
}
#[derive(Debug)]
pub struct RuntimeError {
pub message: String,
pub level: u32,
pub traceback: Vec<TraceEntry>,
}
impl RuntimeError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
level: 0,
traceback: vec![],
}
}
}
pub fn runtime_error(message: impl Into<String>) -> LuaError {
LuaError::Runtime(RuntimeError::new(message))
}
#[derive(Debug, Clone)]
pub struct TraceEntry {
pub source: String,
pub line: u32,
pub name: Option<String>,
}
impl fmt::Display for LuaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Syntax(e) => write!(f, "{e}"),
Self::Runtime(e) => write!(f, "{e}"),
Self::Memory => write!(f, "not enough memory"),
Self::ErrorHandler => write!(f, "error in error handling"),
Self::Io(e) => write!(f, "{e}"),
Self::Yield(_) => write!(f, "cannot yield"),
}
}
}
impl SyntaxError {
#[must_use]
pub fn to_lua_bytes(&self) -> Vec<u8> {
if let Some(ref raw) = self.raw_message {
return raw.clone();
}
self.to_string().into_bytes()
}
}
impl fmt::Display for SyntaxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}: {}",
chunkid(&self.source),
self.line,
self.message
)
}
}
const LUA_IDSIZE: usize = 60;
pub fn chunkid(source: &str) -> String {
if let Some(rest) = source.strip_prefix('=') {
if rest.len() < LUA_IDSIZE {
rest.to_string()
} else {
rest[..LUA_IDSIZE - 1].to_string()
}
} else if let Some(rest) = source.strip_prefix('@') {
if rest.len() < LUA_IDSIZE {
rest.to_string()
} else {
let skip = rest.len() - (LUA_IDSIZE - 4);
format!("...{}", &rest[skip..])
}
} else {
let first_line = source.split('\n').next().unwrap_or(source);
let max_len = LUA_IDSIZE - "[string \"...\"]".len();
if first_line.len() <= max_len && !source.contains('\n') {
format!("[string \"{first_line}\"]")
} else {
let truncated = &first_line[..first_line.len().min(max_len)];
format!("[string \"{truncated}...\"]")
}
}
}
impl fmt::Display for RuntimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl fmt::Display for TraceEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.source, self.line)?;
if let Some(name) = &self.name {
write!(f, " in function '{name}'")?;
}
Ok(())
}
}
impl std::error::Error for LuaError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Syntax(_)
| Self::Runtime(_)
| Self::Memory
| Self::ErrorHandler
| Self::Yield(_) => None,
}
}
}
impl From<std::io::Error> for LuaError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
impl From<SyntaxError> for LuaError {
fn from(err: SyntaxError) -> Self {
Self::Syntax(err)
}
}
impl From<RuntimeError> for LuaError {
fn from(err: RuntimeError) -> Self {
Self::Runtime(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_error_display() {
let err = LuaError::Memory;
assert_eq!(err.to_string(), "not enough memory");
}
#[test]
fn error_handler_display() {
let err = LuaError::ErrorHandler;
assert_eq!(err.to_string(), "error in error handling");
}
#[test]
fn syntax_error_display() {
let err = LuaError::Syntax(SyntaxError {
message: "')' expected near 'end'".into(),
source: "=stdin".into(),
line: 3,
raw_message: None,
});
assert_eq!(err.to_string(), "stdin:3: ')' expected near 'end'");
}
#[test]
fn syntax_error_display_string_source() {
let err = LuaError::Syntax(SyntaxError {
message: "unexpected symbol near 'x'".into(),
source: "break label".into(),
line: 1,
raw_message: None,
});
assert_eq!(
err.to_string(),
"[string \"break label\"]:1: unexpected symbol near 'x'"
);
}
#[test]
fn runtime_error_display() {
let err = LuaError::Runtime(RuntimeError {
message: "attempt to perform arithmetic on a string value".into(),
level: 0,
traceback: vec![],
});
assert_eq!(
err.to_string(),
"attempt to perform arithmetic on a string value"
);
}
#[test]
fn runtime_error_with_location() {
let err = RuntimeError {
message: "stdin:5: attempt to index a nil value".into(),
level: 1,
traceback: vec![TraceEntry {
source: "stdin".into(),
line: 5,
name: Some("foo".into()),
}],
};
assert_eq!(err.to_string(), "stdin:5: attempt to index a nil value");
assert_eq!(err.traceback[0].to_string(), "stdin:5 in function 'foo'");
}
#[test]
fn trace_entry_without_name() {
let entry = TraceEntry {
source: "@test.lua".into(),
line: 10,
name: None,
};
assert_eq!(entry.to_string(), "@test.lua:10");
}
#[test]
fn io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: LuaError = io_err.into();
assert!(matches!(err, LuaError::Io(_)));
assert_eq!(err.to_string(), "file not found");
}
#[test]
fn syntax_error_conversion() {
let syn = SyntaxError {
message: "unexpected symbol".into(),
source: "test".into(),
line: 1,
raw_message: None,
};
let err: LuaError = syn.into();
assert!(matches!(err, LuaError::Syntax(_)));
}
#[test]
fn runtime_error_conversion() {
let rt = RuntimeError {
message: "error".into(),
level: 0,
traceback: vec![],
};
let err: LuaError = rt.into();
assert!(matches!(err, LuaError::Runtime(_)));
}
#[test]
fn error_is_std_error() {
let err = LuaError::Memory;
let _: &dyn std::error::Error = &err;
}
}