Skip to content

Commit 05a21d0

Browse files
committed
Enhance error handling and reporting in XPL interpreter; add ANSI color support for error messages, implement function and variable call statements, and improve parsing logic for better diagnostics.
1 parent a99ff60 commit 05a21d0

File tree

8 files changed

+251
-55
lines changed

8 files changed

+251
-55
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ edition = "2024"
66
[dependencies]
77
xmltree = "0.11.0"
88
thiserror = "2.0.12"
9+
ansi_term = "0.12"

examples/bad_var.xpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<program name="bad_var" version="1.0">
2+
<function name="main">
3+
<body>
4+
<print>foo</print>
5+
</body>
6+
</function>
7+
</program>

src/error.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,61 @@ use thiserror::Error;
44

55
#[derive(Error, Debug)]
66
pub enum XplError {
7-
#[error("IO error: {0}")]
8-
Io(#[from] std::io::Error),
7+
#[error("IO error in {file}: {source}")]
8+
Io {
9+
source: std::io::Error,
10+
file: String,
11+
},
912

10-
#[error("XML parse error: {0}")]
11-
Xml(#[from] xmltree::ParseError),
13+
#[error("XML parse error in {file}: {source}")]
14+
Xml {
15+
source: xmltree::ParseError,
16+
file: String,
17+
},
1218

13-
#[error("Semantic error: {0}")]
14-
Semantic(String),
19+
#[error("{file}:{line}:{col}: {msg}")]
20+
Semantic {
21+
msg: String,
22+
file: String,
23+
line: usize,
24+
col: usize,
25+
},
26+
}
27+
28+
impl XplError {
29+
/// Print the error with colors and source arrow
30+
pub fn pretty_print(&self) {
31+
use ansi_term::Colour::{Blue, Red, Yellow};
32+
match self {
33+
XplError::Io { source, file } => {
34+
eprintln!("{}: {} in file {}", Red.bold().paint("error"), source, file);
35+
}
36+
XplError::Xml { source, file } => {
37+
eprintln!("{}: {} in file {}", Red.bold().paint("error"), source, file);
38+
}
39+
XplError::Semantic {
40+
msg,
41+
file,
42+
line,
43+
col,
44+
} => {
45+
// header
46+
eprintln!("{}: {}", Red.bold().paint("error"), Yellow.paint(msg));
47+
// location
48+
eprintln!(" {} {}:{}:{}", Blue.paint("-->"), file, line, col);
49+
// source context
50+
if let Ok(src) = std::fs::read_to_string(file) {
51+
if let Some(src_line) = src.lines().nth(*line - 1) {
52+
// blank gutter line
53+
eprintln!(" {}", Blue.paint("|"));
54+
// code line without number
55+
eprintln!(" {} {}", Blue.paint("|"), src_line);
56+
// arrow line (align caret under code)
57+
let indent = " ".repeat(col.saturating_sub(1));
58+
eprintln!(" {} {}{}", Blue.paint("|"), indent, Red.paint("^"));
59+
}
60+
}
61+
}
62+
}
63+
}
1564
}

src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub fn run_file(path: &str) -> Result<Vec<String>, XplError> {
1313
if !program.functions.contains_key("main") {
1414
return Ok(Vec::new());
1515
}
16-
let mut vm = vm::VM::new();
16+
let mut vm = vm::VM::new(path.to_string());
1717
let outputs = vm.run(&program)?;
1818
Ok(outputs)
1919
}
@@ -64,4 +64,24 @@ mod tests {
6464
let outputs = run_file(path).unwrap();
6565
assert_eq!(outputs, vec!["x minus 5 is zero".to_string()]);
6666
}
67+
68+
#[test]
69+
fn undefined_variable_error() {
70+
let tmp = "<program name=\"err\" version=\"1.0\"><function name=\"main\"><body><print> y </print></body></function></program>";
71+
let path = std::env::temp_dir().join("err.xpl");
72+
std::fs::write(&path, tmp).unwrap();
73+
let err = run_file(path.to_str().unwrap()).unwrap_err().to_string();
74+
assert!(err.contains("Undefined variable y"));
75+
assert!(err.contains(path.to_str().unwrap()));
76+
}
77+
78+
#[test]
79+
fn undefined_function_error() {
80+
let tmp = "<program name=\"errf\" include=\"examples/math.xpl\" version=\"1.0\"><function name=\"main\"><body><call function=\"none\"><param>1</param></call></body></function></program>";
81+
let path = std::env::temp_dir().join("errf.xpl");
82+
std::fs::write(&path, tmp).unwrap();
83+
let err = run_file(path.to_str().unwrap()).unwrap_err().to_string();
84+
assert!(err.contains("Undefined function none"));
85+
assert!(err.contains(path.to_str().unwrap()));
86+
}
6787
}

src/main.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
fn main() -> Result<(), Box<dyn std::error::Error>> {
1+
fn main() {
22
let args: Vec<String> = std::env::args().collect();
33
if args.len() != 2 {
44
eprintln!("Usage: {} <script.xpl>", args[0]);
55
std::process::exit(1);
66
}
7-
let outputs = xpl::run_file(&args[1])?;
8-
for line in outputs {
9-
println!("{}", line);
7+
match xpl::run_file(&args[1]) {
8+
Ok(outputs) => {
9+
for line in outputs {
10+
println!("{}", line);
11+
}
12+
}
13+
Err(e) => {
14+
e.pretty_print();
15+
std::process::exit(1);
16+
}
1017
}
11-
Ok(())
1218
}

src/parser.rs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub enum Stmt {
3030
else_body: Vec<Stmt>,
3131
},
3232
Return(Expr),
33+
Call(String, Vec<Expr>),
3334
}
3435

3536
#[derive(Debug, Clone, PartialEq)]
@@ -52,17 +53,29 @@ pub enum Expr {
5253

5354
/// Parse an XPL file into a Program AST
5455
pub fn parse_file(path: &str) -> Result<Program, XplError> {
55-
let file = File::open(path)?;
56-
let root = Element::parse(file)?;
56+
let file = File::open(path).map_err(|e| XplError::Io {
57+
source: e,
58+
file: path.to_string(),
59+
})?;
60+
let root = Element::parse(file).map_err(|e| XplError::Xml {
61+
source: e,
62+
file: path.to_string(),
63+
})?;
5764
let mut functions = HashMap::new();
5865
// Process include only for program roots (to load libs)
5966
if root.name == "program" {
6067
if let Some(include_list) = root.attributes.get("include") {
61-
let base = std::path::Path::new(path)
68+
let script_dir = std::path::Path::new(path)
6269
.parent()
6370
.unwrap_or_else(|| std::path::Path::new("."));
6471
for inc in include_list.split(',').map(|s| s.trim()) {
65-
let inc_path = base.join(inc);
72+
// try script-relative first, then workspace-relative
73+
let rel_path = script_dir.join(inc);
74+
let inc_path = if rel_path.exists() {
75+
rel_path
76+
} else {
77+
std::path::Path::new(inc).to_path_buf()
78+
};
6679
let included = parse_file(inc_path.to_str().unwrap())?;
6780
functions.extend(included.functions);
6881
}
@@ -88,8 +101,14 @@ pub fn parse_file(path: &str) -> Result<Program, XplError> {
88101
for stmt_node in &body_elem.children {
89102
if let XMLNode::Element(stmt_elem) = stmt_node {
90103
match stmt_elem.name.as_str() {
104+
"call" => {
105+
// standalone call statement
106+
let expr = parse_expr(stmt_elem)?;
107+
if let Expr::Call(name, args) = expr {
108+
body.push(Stmt::Call(name, args));
109+
}
110+
}
91111
"return" => {
92-
// parse return expression
93112
let expr = if let Some(XMLNode::Element(e)) =
94113
stmt_elem.children.get(0)
95114
{
@@ -101,10 +120,14 @@ pub fn parse_file(path: &str) -> Result<Program, XplError> {
101120
body.push(Stmt::Return(expr));
102121
}
103122
"if" => {
104-
// parse condition
105123
let cond_elem =
106124
stmt_elem.get_child("condition").ok_or_else(|| {
107-
XplError::Semantic("Missing condition".to_string())
125+
XplError::Semantic {
126+
msg: "Missing condition".to_string(),
127+
file: path.to_string(),
128+
line: 0,
129+
col: 0,
130+
}
108131
})?;
109132
// either inner element or text
110133
let cond_expr = if let Some(XMLNode::Element(e)) =
@@ -126,7 +149,12 @@ pub fn parse_file(path: &str) -> Result<Program, XplError> {
126149
// then block
127150
let then_elem =
128151
stmt_elem.get_child("then").ok_or_else(|| {
129-
XplError::Semantic("Missing then block".to_string())
152+
XplError::Semantic {
153+
msg: "Missing then block".to_string(),
154+
file: path.to_string(),
155+
line: 0,
156+
col: 0,
157+
}
130158
})?;
131159
let mut then_body = Vec::new();
132160
for then_node in &then_elem.children {
@@ -154,7 +182,12 @@ pub fn parse_file(path: &str) -> Result<Program, XplError> {
154182
// else block
155183
let else_elem =
156184
stmt_elem.get_child("else").ok_or_else(|| {
157-
XplError::Semantic("Missing else block".to_string())
185+
XplError::Semantic {
186+
msg: "Missing else block".to_string(),
187+
file: path.to_string(),
188+
line: 0,
189+
col: 0,
190+
}
158191
})?;
159192
let mut else_body = Vec::new();
160193
for else_node in &else_elem.children {

0 commit comments

Comments
 (0)