From b889b4c50c87db8260a57ab2628947a6e387c2a8 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 22 Nov 2021 11:43:09 +0100 Subject: [PATCH 1/3] feat: make errors spanned in computations --- computations/Cargo.toml | 3 +- computations/src/lib.rs | 131 ++++++++++++++++++++++++---------------- parser/Cargo.toml | 3 + parser/src/lib.rs | 26 ++++---- parser/src/spanned.rs | 20 +++++- repl/src/main.rs | 18 +++--- 6 files changed, 129 insertions(+), 72 deletions(-) diff --git a/computations/Cargo.toml b/computations/Cargo.toml index ef49a4b..a270d87 100644 --- a/computations/Cargo.toml +++ b/computations/Cargo.toml @@ -4,4 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -parser = { path = "../parser" } \ No newline at end of file +parser = { path = "../parser" } +thiserror = "1.0.30" diff --git a/computations/src/lib.rs b/computations/src/lib.rs index 2e901d4..a2bd2a0 100644 --- a/computations/src/lib.rs +++ b/computations/src/lib.rs @@ -1,10 +1,25 @@ #[allow(dead_code)] mod trees; -use parser::Sym; +use parser::{Spanned, SpannedExt, Sym}; use trees::{ComputationTree, Literal, UnOp}; use crate::trees::{BinOp, Combinator}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ComputationError { + #[error("Expected an operator")] + ExpectedOperator, + #[error("Expression is empty")] + Empty, + #[error("Expression could not fully be computed")] + SymbolsRemaining, + #[error("Unsupported feature: {0}")] + UnsupportedFeature(&'static str), +} + +pub type Error = Spanned; // States for our state machine // WaitForOp -> We are waiting for an operator as the next symbol @@ -27,12 +42,12 @@ struct Accumulator { impl Accumulator { // Build a new accumulator containing only the symbol "s" viewed // as a computation tree - fn new(s: &Sym) -> Result { - Self::to_tree(s).and_then(|acc| { - Ok(Accumulator { + fn new(s: Spanned<&Sym>) -> Result { + Self::to_tree(s).map(|acc| { + Accumulator { state: State::WaitForOp, acc, - }) + } }) } @@ -86,8 +101,8 @@ impl Accumulator { } /// TODO : captures in lambdas ?? - fn count_variables(prog: &Vec) -> u32 { - prog.iter() + fn count_variables<'a, I: IntoIterator>(prog: I) -> u32 { + prog.into_iter() .map(|s| match s { Sym::Var(_) => 1, _ => 0, @@ -96,85 +111,99 @@ impl Accumulator { } /// Convert an arbitrary symbol to a computation tree - fn to_tree<'a>(s: &Sym) -> Result { - match s { + fn to_tree<'a>(s: Spanned<&Sym>) -> Result { + match s.value { Sym::CombS | Sym::CombK | Sym::CombD | Sym::CombI => { - Ok(ComputationTree::CombOp(Self::to_comb(s))) + Ok(ComputationTree::CombOp(Self::to_comb(s.value))) } Sym::Map | Sym::Eq | Sym::Add | Sym::And | Sym::Or | Sym::Filter | Sym::Reduce => { - Ok(ComputationTree::BinOpSym(Self::to_binary(s))) + Ok(ComputationTree::BinOpSym(Self::to_binary(s.value))) + } + Sym::Iota | Sym::Len | Sym::Neg => { + Ok(ComputationTree::UnOpSym(Self::to_unary(s.value))) } - Sym::Iota | Sym::Len | Sym::Neg => Ok(ComputationTree::UnOpSym(Self::to_unary(s))), Sym::Literal(n) => Ok(ComputationTree::Lit(n.clone().into())), Sym::Var(v) => Ok(ComputationTree::Lit(Literal::Var(v.clone()))), - Sym::Lambda(prog) => non_linear_check(prog).and_then(|body| { - Ok(ComputationTree::Lambda { - vars: Self::count_variables(prog), - body: Box::new(body), - }) + Sym::Lambda(prog) => non_linear_check( + prog.iter().map(|s| &s.value).spanned( + prog.iter() + .map(|s| &s.span) + .fold(0..0, |l, r| l.start..r.end), + ), + ) + .map(|body| ComputationTree::Lambda { + vars: Self::count_variables(prog.iter().map(|s| &s.value)), + body: Box::new(body), }), } } /// Given an accumulator and a symbol, next computes a new accumulator /// and consume the symbol to extend the current computation tree. - fn next(self, s: &Sym) -> Result { + fn next(self, s: Spanned<&Sym>) -> Result { match self.state { - State::WaitForOp => match s { + State::WaitForOp => match s.value { Sym::Map | Sym::Eq | Sym::Filter | Sym::Reduce | Sym::Add | Sym::And | Sym::Or => { Ok(Accumulator { - state: State::WaitForVal(Self::to_binary(s)), + state: State::WaitForVal(Self::to_binary(s.value)), ..self }) } Sym::Iota | Sym::Len => Ok(Accumulator { state: State::WaitForOp, acc: ComputationTree::UnaryOp { - op: Self::to_unary(s), + op: Self::to_unary(s.value), lhs: Box::new(self.acc), }, }), - _ => Err("Expected operator"), + _ => Err(ComputationError::ExpectedOperator.spanned(s.span)), }, - State::WaitForVal(op) => Self::to_tree(s).and_then(|tree| { - Ok(Accumulator { - state: State::WaitForOp, - acc: ComputationTree::BinaryOp { - op, - lhs: Box::new(tree), - rhs: Box::new(self.acc), - }, - }) + State::WaitForVal(op) => Self::to_tree(s).map(|tree| Accumulator { + state: State::WaitForOp, + acc: ComputationTree::BinaryOp { + op, + lhs: Box::new(tree), + rhs: Box::new(self.acc), + }, }), } } } // Check that a sequence of symbols is well formed -fn linear_check(prog: &[Sym]) -> Result { - let first = Accumulator::new(&prog[0])?; - let next = &prog[1..]; - next.iter() - .try_fold(first, |acc, s| acc.next(s)) - .and_then(|a| { - // println!("debug {:?}", a); - a.finish().ok_or("Symbols remaining") - }) +fn linear_check<'a, I: IntoIterator>>( + prog: Spanned, +) -> Result { + let mut it = prog.value.into_iter(); + let acc = Accumulator::new( + it.next() + .ok_or(ComputationError::Empty.spanned(prog.span.clone()))?, + )?; + it.try_fold(acc, |acc, s| acc.next(s)).and_then(|a| { + // println!("debug {:?}", a); + a.finish() + .ok_or(ComputationError::SymbolsRemaining.spanned(prog.span)) + }) } // Check that a sequence of symbols is well formed (in the context of a lambda) -fn non_linear_check(_prog: &[Sym]) -> Result { - Err("TODO: Lambdas not supported") +fn non_linear_check<'a, I: IntoIterator>( + prog: Spanned, +) -> Result { + Err(ComputationError::UnsupportedFeature("non_linear_check").spanned(prog.span)) } /// Check that an ULP program is well formed and returns its associated /// computation tree -pub fn check(mut prog: Vec) -> Result { - if prog.len() == 0 { - Err("No symbols") +pub fn check(prog: Vec>) -> Result { + let span = prog + .iter() + .map(|s| s.span.clone()) + .fold(0..0, |l, r| l.start..r.end); + if prog.is_empty() { + Err(ComputationError::Empty.spanned(span)) } else { - prog.reverse(); - linear_check(&prog) + linear_check(prog.iter().map(|s| s.as_ref()).spanned(span)) } } @@ -194,7 +223,7 @@ mod test { Sym::Iota, Sym::Literal(Lit::Num("2".to_string())), ]; - let res = check(prog); + let res = check(prog.into_iter().map(|s| s.spanned(0..0)).collect()); println!("result: {:?}", res); assert!(res.is_ok()) } @@ -206,9 +235,9 @@ mod test { Sym::Add, Sym::Map, Sym::Iota, - Sym::Literal(Lit::Num("2".to_string())) + Sym::Literal(Lit::Num("2".to_string())), ]; - let err = check(prog); + let err = check(prog.into_iter().map(|s| s.spanned(0..0)).collect()); println!("result: {:?}", err); assert!(err.is_err()) } @@ -223,7 +252,7 @@ mod test { Sym::Iota, Sym::Literal(Lit::Num("2".to_string())), ]; - let err = check(prog); + let err = check(prog.into_iter().map(|s| s.spanned(0..0)).collect()); println!("result: {:?}", err); assert!(err.is_ok()) } diff --git a/parser/Cargo.toml b/parser/Cargo.toml index e256f03..338701d 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -8,3 +8,6 @@ edition = "2021" [dependencies] ariadne = "0.1.3" chumsky = "0.5.0" + +[dev-dependencies] +insta = "1.8.0" diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 7761f22..e57bc48 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -8,6 +8,8 @@ use report::Report; use report::{report_of_char_error, report_of_token_error}; use token::lexer; +pub use spanned::*; + #[derive(Clone, Debug, PartialEq)] pub enum Lit { Num(String), @@ -32,11 +34,11 @@ pub enum Sym { Add, Literal(Lit), Var(u32), - Lambda(Vec), + Lambda(Vec>), } impl Sym { - pub fn lambda>>(inner: I) -> Option { + pub fn lambda>>>(inner: I) -> Option { Some(Self::Lambda(inner.into_iter().collect::>>()?)) } } @@ -51,7 +53,7 @@ fn literal() -> impl Parser> { recursive(|lit| lit.repeated().at_least(1).delimited_by(Bracket(L), Bracket(R)).map(Lit::List).or(int)) } -fn parser() -> impl Parser>, Error = Simple> { +fn parser() -> impl Parser>>, Error = Simple> { use token::Dir::*; use Token::*; let var = filter_map(|span, tok| match tok { @@ -62,7 +64,7 @@ fn parser() -> impl Parser>, Error = Simple> { recursive(|instr| { instr .delimited_by(Brace(L), Brace(R)) - .map(|v| Sym::lambda(v)) + .map_with_span(|v, span| Sym::lambda(v).map(|l| l.spanned(span))) .or(just(Ident("K".to_string())) .to(Sym::CombK) .or(just(Ident("S".to_string())).to(Sym::CombS)) @@ -80,14 +82,14 @@ fn parser() -> impl Parser>, Error = Simple> { .or(just(Op("\\".to_string())).to(Sym::Filter)) .or(lit) .or(var) - .map(Some)) + .map_with_span(|s, span| Some(s.spanned(span)))) .recover_with(nested_delimiters(Brace(L), Brace(R), [], |_| None)) .repeated() }) .map(|v| v.into_iter().collect::>>()) } -pub fn parse(src_id: impl Into, input: &str) -> (Option>, Vec) { +pub fn parse(src_id: impl Into, input: &str) -> (Option>>, Vec) { let src_id = src_id.into(); let slen = input.len(); let (tokens, tokerr) = lexer().then_ignore(end()).parse_recovery(input); @@ -125,13 +127,15 @@ mod tests { macro_rules! assert_parse { ($input:expr, [$($e:expr),*]) => { { - use ariadne::Source; let input = $input; let (res, err) = parse("", input); - for report in err { - report.eprint(("".into(), Source::from(input))).unwrap(); - } - assert_eq!(res, Some(vec![$($e),*])); + insta::assert_display_snapshot!(err.into_iter().map(|r| { + use ariadne::Source; + let mut s = ::std::io::Cursor::new(Vec::new()); + r.write(("".into(), Source::from(input)), &mut s).unwrap(); + String::from_utf8_lossy(&s.into_inner()).to_string() + }).collect::>().join("\n")); + insta::assert_debug_snapshot!(res); } }; } diff --git a/parser/src/spanned.rs b/parser/src/spanned.rs index e53bb4a..3f59f1e 100644 --- a/parser/src/spanned.rs +++ b/parser/src/spanned.rs @@ -1,4 +1,4 @@ -use std::ops::{Range, Deref}; +use std::{error::Error, fmt::{self, Display}, ops::{Range, Deref}}; pub type Span = Range; @@ -22,6 +22,24 @@ impl From> for (T, Span) { } } +impl fmt::Display for Spanned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}: {}", self.span.start, self.span.end, self.value) + } +} + +impl Error for Spanned { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.value.source() + } +} + +impl Spanned { + pub fn as_ref(&self) -> Spanned<&T> { + (&self.value).spanned(self.span.clone()) + } +} + pub trait SpannedExt: Sized { fn spanned(self, span: Span) -> Spanned { Spanned { span, value: self } diff --git a/repl/src/main.rs b/repl/src/main.rs index f984b08..8d4ca59 100644 --- a/repl/src/main.rs +++ b/repl/src/main.rs @@ -1,10 +1,10 @@ -use ariadne::{ReportKind, Source}; +use ariadne::{Label, ReportKind, Source}; use computations::check; use parser::parse; -use rustyline::{Editor, error::ReadlineError}; +use rustyline::{error::ReadlineError, Editor}; use std::ops::Range; -const HISTORYFILE: &'static str = "/tmp/ulp-repl.history"; +const HISTORYFILE: &str = "/tmp/ulp-repl.history"; type SrcId = (String, Range); type Report = ariadne::Report; @@ -19,21 +19,23 @@ fn main() { rl.add_history_entry(&line); let (ast, errors) = parse("", &line); for err in errors { - err.eprint(("".to_string(), Source::from(&line))).unwrap(); + err.eprint(("".to_string(), Source::from(&line))) + .unwrap(); } if let Some(ast) = ast { match check(ast) { Ok(comp) => println!("Computation {:#?}", comp), - Err(err) => Report::build(ReportKind::Error, "", 0) - .with_message(format!("Check error: {}", err)) + Err(err) => Report::build(ReportKind::Error, "", err.span.start) + .with_label(Label::new(("".into(), err.span)).with_color(ariadne::Color::Red).with_message(err.value.to_string())) + .with_message("Check error") .with_note("The structure is correct, however ULP could not figure out how to compute the expression.") .finish() .eprint(("".to_string(), Source::from(&line))) .unwrap(), } } - }, - Err(ReadlineError::Interrupted) => {}, + } + Err(ReadlineError::Interrupted) => {} Err(ReadlineError::Eof) => break, Err(err) => eprintln!("Fatal error: {}", err), } From 81efc4edc7be771d4e1362e998b9715cff85afdb Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 23 Nov 2021 10:34:23 +0100 Subject: [PATCH 2/3] feat: put utility types in utils crate --- Cargo.toml | 2 +- utils/Cargo.toml | 9 ++ utils/src/lib.rs | 210 ++++++++++++++++++++++++++++++++++++++++++++ utils/src/report.rs | 39 ++++++++ 4 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 utils/Cargo.toml create mode 100644 utils/src/lib.rs create mode 100644 utils/src/report.rs diff --git a/Cargo.toml b/Cargo.toml index a426954..a436f99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["parser", "computations", "repl"] +members = ["parser", "computations", "repl", "utils"] diff --git a/utils/Cargo.toml b/utils/Cargo.toml new file mode 100644 index 0000000..639df54 --- /dev/null +++ b/utils/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "utils" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ariadne = "*" diff --git a/utils/src/lib.rs b/utils/src/lib.rs new file mode 100644 index 0000000..d810c66 --- /dev/null +++ b/utils/src/lib.rs @@ -0,0 +1,210 @@ +use std::{ + error::Error, + fmt, + ops::{Deref, Range}, + rc::Rc, +}; + +pub mod report; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl From> for Span { + fn from(r: Range) -> Self { + Self { + start: r.start, + end: r.end, + } + } +} + +impl Into> for Span { + fn into(self) -> Range { + self.start..self.end + } +} + +impl fmt::Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.start, self.end) + } +} + +impl Span { + pub fn merge(self, other: Self) -> Self { + Self { + start: self.start.min(other.start), + end: self.end.max(other.end), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Position { + pub reference: Option>, + pub span: Span, +} + +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}:{}", + self.reference.as_deref().unwrap_or(""), + self.span + ) + } +} + +impl Position { + pub fn with_reference>>(mut self, reference: S) -> Self { + self.reference = Some(reference.into()); + self + } + + pub fn with_span>(mut self, span: S) -> Self { + self.span = span.into(); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Positioned { + pos: Position, + value: T, +} + +impl fmt::Display for Positioned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.pos, self.value) + } +} + +impl Error for Positioned { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.value.source() + } +} + +impl Position { + pub fn merge(self, other: Self) -> Self { + Self { + reference: self.reference.or(other.reference), + span: self.span.merge(other.span), + } + } +} + +impl Deref for Positioned { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Positioned { + pub fn map(self, map: impl FnOnce(T) -> U) -> Positioned { + Positioned { + pos: self.pos, + value: map(self.value), + } + } + + pub fn as_ref(&self) -> Positioned<&T> { + Positioned { + pos: self.pos.clone(), + value: &self.value, + } + } + + pub fn as_deref(&self) -> Positioned<&T::Target> + where + T: Deref, + { + self.as_ref().map(|r| r.deref()) + } + + pub fn into_inner(self) -> T { + self.value + } + + pub fn with_reference>>(self, reference: S) -> Self { + Self { + pos: self.pos.with_reference(reference), + value: self.value, + } + } + + pub fn with_span>(self, span: S) -> Self { + Self { + pos: self.pos.with_span(span), + value: self.value, + } + } +} + +impl Positioned> { + pub fn from_positioned(values: Vec>) -> Self { + let (pos, values): (Vec<_>, Vec<_>) = values.into_iter().map(|p| (p.pos, p.value)).unzip(); + let pos = pos.into_iter().fold(Position::default(), Position::merge); + Self { pos, value: values } + } +} + +impl Positioned> { + pub fn transpose(self) -> Result, Positioned> { + let Self { pos, value } = self; + value + .map(|t| Positioned { + pos: pos.clone(), + value: t, + }) + .map_err(|e| Positioned { pos, value: e }) + } +} + +impl Positioned> { + pub fn transpose(self) -> Option> { + let Self { pos, value } = self; + value.map(|value| Positioned { pos, value }) + } +} + +pub trait PositionedExt: Sized { + fn positioned>, Sp: Into>( + self, + span: Sp, + reference: St, + ) -> Positioned { + Positioned { + pos: Position { + reference: Some(reference.into()), + span: span.into(), + }, + value: self, + } + } + + fn spanned>(self, span: S) -> Positioned { + Positioned { + pos: Position::default().with_span(span), + value: self, + } + } + + fn referenced>>(self, reference: S) -> Positioned { + Positioned { + pos: Position { + reference: Some(reference.into()), + span: Span::default(), + }, + value: self, + } + } +} + +impl PositionedExt for T {} diff --git a/utils/src/report.rs b/utils/src/report.rs new file mode 100644 index 0000000..27fe9ec --- /dev/null +++ b/utils/src/report.rs @@ -0,0 +1,39 @@ +use std::{ops::Deref, rc::Rc}; + +use ariadne::ReportKind; + +use crate::{Position, Positioned}; + +pub type SourceId = Position; +pub type Label = ariadne::Label; +pub type ReportBuilder = ariadne::ReportBuilder; +pub type Report = ariadne::Report; + +impl ariadne::Span for Position { + type SourceId = Option>; + + fn source(&self) -> &Self::SourceId { + &self.reference + } + + fn start(&self) -> usize { + self.span.start + } + + fn end(&self) -> usize { + self.span.end + } +} + +impl Positioned { + pub fn into_report(self, kind: ReportKind) -> ReportBuilder { + Report::build(kind, self.pos.reference, self.pos.span.start) + .with_message(self.value.to_string()) + } +} + +impl Into