diff --git a/Cargo.lock b/Cargo.lock index be01c42..9e579ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,7 @@ dependencies = [ name = "hir" version = "0.1.0" dependencies = [ + "levenshtein", "parser", ] @@ -203,6 +204,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "lexer" version = "0.1.0" diff --git a/crates/diagnostic/src/lib.rs b/crates/diagnostic/src/lib.rs index f41330c..947a6c8 100644 --- a/crates/diagnostic/src/lib.rs +++ b/crates/diagnostic/src/lib.rs @@ -116,5 +116,36 @@ impl Diagnostics { report.finish().print(Source::from(&src)).unwrap(); }); // End errors reporting + + let lower_error = self.errors.iter().filter_map(|kind| match kind { + Kind::LoweringError(error) => Some(error.clone()), + _ => None, + }); + + lower_error.into_iter() + .for_each(|e| { + let span = &e.span; + let message = &e.message; + + let report = Report::build(ReportKind::Error, (), span.start) + .with_message( + format!("{}", message) + ) + .with_label( + Label::new(span.clone()) + .with_message( + format!("{}", message) + ) + .with_color(Color::Red) + ); + + if let Some(note) = &e.note { + report + .with_note(note) + .finish().print(Source::from(&src)).unwrap(); + } else { + report.finish().print(Source::from(&src)).unwrap(); + } + }); } } \ No newline at end of file diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml index 178f2d3..76a59c5 100644 --- a/crates/hir/Cargo.toml +++ b/crates/hir/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -parser = { path = "../parser" } \ No newline at end of file +parser = { path = "../parser" } +levenshtein = "1.0.5" # Used for error reporting \ No newline at end of file diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index a8e8c07..a12fbfc 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -29,7 +29,8 @@ pub struct IR { #[derive(Debug)] pub struct LoweringError { pub span: Range, - pub message: String + pub message: String, + pub note: Option, } impl IR { @@ -82,7 +83,7 @@ pub fn expr_to_ir(expr: &Expr) -> (Option, Option) { let name = match &name.0 { Expr::Identifier(s) => s.clone(), // Should never happen because the parser should have caught this - _ => return (None, Some(LoweringError { span: name.1.clone(), message: "Expected identifier".to_string() })) + _ => return (None, Some(LoweringError { span: name.1.clone(), message: "Expected identifier".to_string(), note: None })) }; let mut largs = Vec::new(); // `largs` stand for lowered args // Iterate over args @@ -101,7 +102,7 @@ pub fn expr_to_ir(expr: &Expr) -> (Option, Option) { if_err_return!(value.1); let value = value.0.unwrap(); - let ir_kind = IRKind::Define { name: name.clone(), type_hint: type_hint.clone(), value: Box::new(value) }; + let ir_kind = IRKind::Define { name: name.clone(), type_hint: gen_type_hint(type_hint), value: Box::new(value) }; return (Some(ir_kind), None); }, @@ -110,10 +111,14 @@ pub fn expr_to_ir(expr: &Expr) -> (Option, Option) { Expr::Identifier(s) => { if INTRINSICS.contains(&s.as_str()) { s.clone() } else { - return (None, Some(LoweringError { span: name.1.clone(), message: format!("Unknown intrinsic: {}", s) })); + return (None, Some(LoweringError { + span: name.1.clone(), + message: format!("Unknown intrinsic: `{}`", s), + note: Some(format!("Did you mean: {}?", closet_intrinsic(s.to_string()))) + })); } } - _ => return (None, Some(LoweringError { span: name.1.clone(), message: "Expected identifier".to_string() })) + _ => return (None, Some(LoweringError { span: name.1.clone(), message: "Expected identifier".to_string(), note: None })) }; let mut largs = Vec::new(); for arg in &args.0 { @@ -193,4 +198,18 @@ fn gen_type_hint(type_hint: &str) -> String { "string" => "std::string".to_string(), _ => { dbg!(type_hint); todo!() } } +} + +// Get the closet intrinsic name to the given name +fn closet_intrinsic(got: String) -> String { + let mut closest = String::new(); + let mut closest_dist = std::usize::MAX; + for intrinsic in INTRINSICS.iter() { + let dist = levenshtein::levenshtein(got.as_str(), intrinsic); + if dist < closest_dist { + closest = intrinsic.to_string(); + closest_dist = dist; + } + } + closest } \ No newline at end of file diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 0f71b7d..233eafd 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -43,9 +43,9 @@ fn expr_parser() -> impl Parser>, Error = Simple }).labelled("identifier"); let literal = filter_map(|span, token| match token { - Token::Int(i) => Ok((Expr::Int(i), span)), + Token::Int(i) => Ok((Expr::Int(i), span)), Token::Boolean(b) => Ok((Expr::Boolean(b), span)), - Token::String(s) => Ok((Expr::String(s), span)), + Token::String(s) => Ok((Expr::String(s), span)), _ => Err(Simple::expected_input_found(span, Vec::new(), Some(token))), }).labelled("literal"); diff --git a/example/err.hz b/example/err.hz index 0de1a37..cdf2c48 100644 --- a/example/err.hz +++ b/example/err.hz @@ -1,2 +1,3 @@ --- Can't use Int as identifier -let 123 = 123; \ No newline at end of file +fun main: int = do + @writse(); -- Unknown intrinsic +end; \ No newline at end of file diff --git a/example/readwrite.hz b/example/readwrite.hz new file mode 100644 index 0000000..4f394aa --- /dev/null +++ b/example/readwrite.hz @@ -0,0 +1,9 @@ +fun main: int = do + @write("Enter your name: "); + let name: string = ""; + @read(name); + + @write("Hello "); + @write(name); + @write("!"); +end; \ No newline at end of file