Skip to content

Commit a2da124

Browse files
committed
better errors
1 parent a98023e commit a2da124

File tree

8 files changed

+120
-10
lines changed

8 files changed

+120
-10
lines changed

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
description = "High-performance modern Wilkinson's formula parsing for statistical models. Parses R-style formulas into structured JSON metadata supporting linear models, mixed effects, and complex statistical specifications."
33
name = "fiasto"
4-
version = "0.2.1"
4+
version = "0.2.2"
55
edition = "2021"
66
authors = ["Alex Hallam <alexhallam6.28@gmail.com>"]
77
license = "MIT"
@@ -24,3 +24,4 @@ serde = { version = "1.0", features = ["derive"] }
2424
serde_json = "1.0"
2525
thiserror = "1.0"
2626
logos = "0.15.1"
27+
owo-colors = "4.2.2"

examples/e01.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use fiasto::parse_formula;
2+
3+
fn main() -> Result<(), Box<dyn std::error::Error>> {
4+
let input = "y ~ x +++";
5+
6+
let result = parse_formula(input)?;
7+
println!("{}", serde_json::to_string_pretty(&result)?);
8+
9+
Ok(())
10+
}

examples/pretty_error.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use fiasto::internal::parser::Parser;
2+
3+
// This example intentionally uses a malformed formula so we can see the pretty error output.
4+
fn main() -> Result<(), Box<dyn std::error::Error>> {
5+
// trailing '+' will cause a parse error
6+
let input = "y ~ x +";
7+
let mut parser = Parser::new(input)?;
8+
match parser.parse_formula() {
9+
Ok(_) => println!("parsed ok (unexpected)"),
10+
Err(e) => {
11+
// Print the colored, pretty error to stderr
12+
eprintln!("{}", parser.pretty_error(&e));
13+
}
14+
}
15+
Ok(())
16+
}

examples/test_pretty.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use fiasto::parse_formula;
2+
3+
fn main() {
4+
// Test different error scenarios
5+
let test_cases = vec![
6+
"y ~ x +", // trailing plus
7+
"y ~ + x", // leading plus
8+
"~ x", // missing response
9+
"y ~ x (", // unmatched paren
10+
"y ~", // incomplete formula
11+
];
12+
13+
for (i, formula) in test_cases.iter().enumerate() {
14+
println!("\n=== Test case {} ===", i + 1);
15+
println!("Formula: {}", formula);
16+
if let Err(_) = parse_formula(formula) {
17+
// Error is already printed by parse_formula via eprintln!
18+
}
19+
}
20+
}

src/internal/parse_term.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub fn parse_term<'a>(tokens: &'a [(Token, &'a str)], pos: &mut usize) -> Result
143143
| Token::FunctionStart
144144
)
145145
},
146-
"Function token or ColumnName",
146+
"Function or ColumnName",
147147
)?;
148148
if crate::internal::matches::matches(tokens, pos, |t| matches!(t, Token::FunctionStart)) {
149149
let fname = match tok {

src/internal/parser.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ use crate::internal::{
5151
lexer::Token,
5252
};
5353

54+
use owo_colors::OwoColorize;
55+
5456
/// Parser for statistical formulas
5557
///
5658
/// The parser converts formula strings into Abstract Syntax Trees (ASTs).
@@ -113,22 +115,63 @@ impl<'a> Parser<'a> {
113115
crate::internal::new::new(input)
114116
}
115117

118+
/// Pretty-print a parse error with context (tokens, last-consumed lexeme, expected/found)
119+
///
120+
/// This produces a colored, human-friendly message useful for CLI output.
121+
pub fn pretty_error(&self, err: &ParseError) -> String {
122+
match err {
123+
ParseError::Lex(s) => {
124+
format!("{}\n\n{}\n", "Lexing error".red().bold(), s)
125+
}
126+
ParseError::Eoi => {
127+
format!("{}\n\n{}\n", "Unexpected end of input".red().bold(), "the formula ended earlier than expected")
128+
}
129+
ParseError::Unexpected { expected, found: _ } => {
130+
let mut out = String::new();
131+
132+
// Header
133+
out.push_str(&format!("{}\n", "Syntax error- Unexpected Token".red().bold()));
134+
135+
// Formula: just print the original formula uncolored
136+
out.push_str(&format!("Formula: {}\n", self.input));
137+
138+
// Show: previous successful lexemes in green then failed lexeme in red
139+
out.push_str("Show: ");
140+
for i in 0..self.pos {
141+
if let Some((_, lex)) = self.tokens.get(i) {
142+
out.push_str(&format!("{} ", lex.green()));
143+
}
144+
}
145+
let failed = self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or("<eoi>");
146+
out.push_str(&format!("{}\n", failed.red()));
147+
148+
// Expected Token: list expected tokens
149+
out.push_str(&format!("Expected Token: {}\n", expected.to_string()));
150+
151+
out
152+
}
153+
ParseError::Syntax(s) => {
154+
format!("{}\n\n{}\n", "Syntax error".red().bold(), s)
155+
}
156+
}
157+
}
158+
116159
/// Parses the formula and returns the complete AST information
117160
///
118161
/// This method performs the syntactic analysis of the tokenized formula
119-
/// and returns the parsed components.
162+
/// and returns the structured representation needed for statistical modeling.
120163
///
121164
/// # Returns
122-
///
165+
///
123166
/// A tuple containing:
124-
/// * `String` - The response variable name
167+
/// * `String` - The response variable (left side of ~)
125168
/// * `Vec<Term>` - All terms in the formula (fixed effects, random effects, etc.)
126169
/// * `bool` - Whether the model includes an intercept
127170
/// * `Option<Family>` - The distribution family (if specified)
128171
///
129172
/// # Examples
130-
///
131-
/// ```rust
173+
///
174+
/// ```rust
132175
/// use fiasto::internal::parser::Parser;
133176
///
134177
/// let formula = "y ~ x + (1 | group), family = gaussian";
@@ -142,6 +185,12 @@ impl<'a> Parser<'a> {
142185
pub fn parse_formula(
143186
&mut self,
144187
) -> Result<(String, Vec<Term>, bool, Option<Family>), ParseError> {
145-
crate::internal::parse_formula::parse_formula(&self.tokens, &mut self.pos)
188+
match crate::internal::parse_formula::parse_formula(&self.tokens, &mut self.pos) {
189+
Ok(v) => Ok(v),
190+
Err(e) => {
191+
// Return the original error unchanged so pretty_error can handle it properly
192+
Err(e)
193+
}
194+
}
146195
}
147196
}

src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,14 @@ use serde_json::Value;
282282
/// - Fast pattern matching
283283
pub fn parse_formula(formula: &str) -> Result<Value, Box<dyn std::error::Error>> {
284284
let mut p = Parser::new(formula)?;
285-
let (response, terms, has_intercept, family_opt) = p.parse_formula()?;
285+
let (response, terms, has_intercept, family_opt) = match p.parse_formula() {
286+
Ok(v) => v,
287+
Err(e) => {
288+
// Print pretty, colored error by default for CLI users
289+
eprintln!("{}", p.pretty_error(&e));
290+
return Err(Box::new(e));
291+
}
292+
};
286293

287294
let mut mb = MetaBuilder::new();
288295
mb.push_response(&response);

0 commit comments

Comments
 (0)