From 7263507e84060933a36b9f8b8a3d3ad572eeda43 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 23 Mar 2024 17:07:41 -0400 Subject: [PATCH] Refine error reports --- Cargo.lock | 1 + dust-lang/src/abstract_tree/type.rs | 8 +- dust-lang/src/built_in_functions.rs | 48 ++---- dust-lang/src/error.rs | 73 +++++----- dust-lang/src/lexer.rs | 3 + dust-lang/src/parser.rs | 6 +- dust-shell/Cargo.toml | 1 + dust-shell/src/cli.rs | 7 +- dust-shell/src/error.rs | 218 ++++++++++++++++++++++++++++ dust-shell/src/main.rs | 22 ++- std/thread.ds | 5 + 11 files changed, 302 insertions(+), 90 deletions(-) create mode 100644 dust-shell/src/error.rs create mode 100644 std/thread.ds diff --git a/Cargo.lock b/Cargo.lock index dafa4bc..078a39b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,6 +384,7 @@ dependencies = [ name = "dust-shell" version = "0.1.0" dependencies = [ + "ariadne", "clap", "colored", "dust-lang", diff --git a/dust-lang/src/abstract_tree/type.rs b/dust-lang/src/abstract_tree/type.rs index e7ccf66..bdc91ad 100644 --- a/dust-lang/src/abstract_tree/type.rs +++ b/dust-lang/src/abstract_tree/type.rs @@ -144,11 +144,11 @@ impl Display for Type { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Type::Any => write!(f, "any"), - Type::Boolean => write!(f, "boolean"), + Type::Boolean => write!(f, "bool"), Type::Float => write!(f, "float"), - Type::Integer => write!(f, "integer"), + Type::Integer => write!(f, "int"), Type::List => write!(f, "list"), - Type::ListOf(item_type) => write!(f, "list of {item_type}"), + Type::ListOf(item_type) => write!(f, "list({item_type})"), Type::ListExact(item_types) => { write!(f, "[")?; @@ -165,7 +165,7 @@ impl Display for Type { Type::Map => write!(f, "map"), Type::None => write!(f, "none"), Type::Range => write!(f, "range"), - Type::String => write!(f, "string"), + Type::String => write!(f, "str"), Type::Function { parameter_types, return_type, diff --git a/dust-lang/src/built_in_functions.rs b/dust-lang/src/built_in_functions.rs index 05ea6a0..08dbf92 100644 --- a/dust-lang/src/built_in_functions.rs +++ b/dust-lang/src/built_in_functions.rs @@ -5,12 +5,10 @@ use std::{ time::Duration, }; -use rand::{thread_rng, Rng}; - use crate::{ - abstract_tree::{Action, Identifier, Type, WithPosition}, + abstract_tree::{Action, Type}, context::Context, - error::{RuntimeError, ValidationError}, + error::RuntimeError, value::ValueInner, Value, }; @@ -18,6 +16,7 @@ use crate::{ #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub enum BuiltInFunction { ReadLine, + Sleep, WriteLine, } @@ -25,6 +24,7 @@ impl BuiltInFunction { pub fn name(&self) -> &'static str { match self { BuiltInFunction::ReadLine => todo!(), + BuiltInFunction::Sleep => todo!(), BuiltInFunction::WriteLine => todo!(), } } @@ -47,32 +47,6 @@ impl BuiltInFunction { pub fn call(&self, arguments: Vec, context: &Context) -> Result { match self { - BuiltInFunction::ReadLine => { - let string = arguments.get(0).unwrap(); - - if let ValueInner::String(_string) = string.inner().as_ref() { - // let integer = string.parse(); - - todo!() - - // Ok(Action::Return(Value::integer(integer))) - } else { - let mut actual = Vec::with_capacity(arguments.len()); - - for value in arguments { - let r#type = value.r#type(context)?; - - actual.push(r#type); - } - - Err(RuntimeError::ValidationFailure( - ValidationError::WrongArguments { - expected: vec![Type::String], - actual, - }, - )) - } - } // "INT_RANDOM_RANGE" => { // let range = arguments.get(0).unwrap(); @@ -91,18 +65,18 @@ impl BuiltInFunction { Ok(Action::Return(Value::string(input))) } + BuiltInFunction::Sleep => { + if let ValueInner::Integer(milliseconds) = arguments[0].inner().as_ref() { + thread::sleep(Duration::from_millis(*milliseconds as u64)); + } + + Ok(Action::None) + } BuiltInFunction::WriteLine => { println!("{}", arguments[0]); Ok(Action::None) } - // "SLEEP" => { - // if let ValueInner::Integer(milliseconds) = arguments[0].inner().as_ref() { - // thread::sleep(Duration::from_millis(*milliseconds as u64)); - // } - - // Ok(Action::None) - // } _ => { todo!() } diff --git a/dust-lang/src/error.rs b/dust-lang/src/error.rs index bc73360..c5baa6e 100644 --- a/dust-lang/src/error.rs +++ b/dust-lang/src/error.rs @@ -1,6 +1,6 @@ -use std::{io, sync::PoisonError}; +use std::{fmt::Debug, hash::Hash, io, ops::Range, sync::PoisonError}; -use ariadne::{sources, Color, Fmt, Label, Report, ReportKind}; +use ariadne::{Color, Fmt, Label, Report, ReportKind}; use chumsky::{prelude::Rich, span::Span}; use crate::{ @@ -31,7 +31,10 @@ pub enum Error { } impl Error { - pub fn build_report(self, source: &str) -> Result, io::Error> { + pub fn build_report<'id, Id: Debug + Hash + Eq + Clone>( + self, + source_id: Id, + ) -> Result)>, io::Error> { let (mut builder, validation_error, error_position) = match self { Error::Parse { expected, @@ -47,12 +50,12 @@ impl Error { ( Report::build( ReportKind::Custom("Parsing Error", Color::Yellow), - "input", + source_id.clone(), span.1, ) .with_message(description) .with_label( - Label::new(("input", span.0..span.1)) + Label::new((source_id.clone(), span.0..span.1)) .with_message(reason) .with_color(Color::Red), ), @@ -74,12 +77,12 @@ impl Error { ( Report::build( ReportKind::Custom("Lexing Error", Color::Yellow), - "input", + source_id.clone(), span.1, ) .with_message(description) .with_label( - Label::new(("input", span.0..span.1)) + Label::new((source_id.clone(), span.0..span.1)) .with_message(reason) .with_color(Color::Red), ), @@ -90,7 +93,7 @@ impl Error { Error::Runtime { error, position } => ( Report::build( ReportKind::Custom("Runtime Error", Color::Red), - "input", + source_id.clone(), position.1, ) .with_message("An error occured that forced the program to exit.") @@ -110,7 +113,7 @@ impl Error { Error::Validation { error, position } => ( Report::build( ReportKind::Custom("Validation Error", Color::Magenta), - "input", + source_id.clone(), position.1, ) .with_message("The syntax is valid but this code is not sound.") @@ -126,22 +129,22 @@ impl Error { if let Some(validation_error) = validation_error { match validation_error { ValidationError::ExpectedBoolean { actual, position } => { - builder.add_label(Label::new(("input", position.0..position.1)).with_message( - format!( + builder.add_label( + Label::new((source_id, position.0..position.1)).with_message(format!( "Expected {} but got {}.", "boolean".fg(type_color), actual.fg(type_color) - ), - )); + )), + ); } ValidationError::ExpectedIntegerOrFloat(position) => { - builder.add_label(Label::new(("input", position.0..position.1)).with_message( - format!( + builder.add_label( + Label::new((source_id, position.0..position.1)).with_message(format!( "Expected {} or {}.", "integer".fg(type_color), "float".fg(type_color) - ), - )); + )), + ); } ValidationError::RwLockPoison(_) => todo!(), ValidationError::TypeCheck { @@ -154,15 +157,17 @@ impl Error { builder = builder.with_message("A type conflict was found."); builder.add_labels([ - Label::new(("input", expected_postion.0..expected_postion.1)).with_message( - format!("Type {} established here.", expected.fg(type_color)), - ), - Label::new(("input", actual_position.0..actual_position.1)) + Label::new((source_id.clone(), expected_postion.0..expected_postion.1)) + .with_message(format!( + "Type {} established here.", + expected.fg(type_color) + )), + Label::new((source_id, actual_position.0..actual_position.1)) .with_message(format!("Got type {} here.", actual.fg(type_color))), ]); } ValidationError::VariableNotFound(identifier) => builder.add_label( - Label::new(("input", error_position.0..error_position.1)).with_message( + Label::new((source_id, error_position.0..error_position.1)).with_message( format!( "Variable {} does not exist in this context.", identifier.fg(identifier_color) @@ -170,7 +175,7 @@ impl Error { ), ), ValidationError::CannotIndex { r#type, position } => builder.add_label( - Label::new(("input", position.0..position.1)) + Label::new((source_id, position.0..position.1)) .with_message(format!("Cannot index into a {}.", r#type.fg(type_color))), ), ValidationError::CannotIndexWith { @@ -186,12 +191,14 @@ impl Error { )); builder.add_labels([ - Label::new(("input", collection_position.0..collection_position.1)) - .with_message(format!( - "This has type {}.", - collection_type.fg(type_color), - )), - Label::new(("input", index_position.0..index_position.1)) + Label::new(( + source_id.clone(), + collection_position.0..collection_position.1, + )) + .with_message( + format!("This has type {}.", collection_type.fg(type_color),), + ), + Label::new((source_id, index_position.0..index_position.1)) .with_message(format!("This has type {}.", index_type.fg(type_color),)), ]) } @@ -203,13 +210,7 @@ impl Error { } } - let mut output = Vec::new(); - - builder - .finish() - .write_for_stdout(sources([("input", source)]), &mut output)?; - - Ok(output) + Ok(builder.finish()) } } diff --git a/dust-lang/src/lexer.rs b/dust-lang/src/lexer.rs index dcc13db..0731d77 100644 --- a/dust-lang/src/lexer.rs +++ b/dust-lang/src/lexer.rs @@ -39,6 +39,7 @@ impl<'src> Display for Token<'src> { #[derive(Copy, Clone, Debug, PartialEq)] pub enum BuiltInIdentifier { ReadLine, + Sleep, WriteLine, } @@ -46,6 +47,7 @@ impl Display for BuiltInIdentifier { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { BuiltInIdentifier::ReadLine => write!(f, "__READ_LINE__"), + BuiltInIdentifier::Sleep => write!(f, "__SLEEP__"), BuiltInIdentifier::WriteLine => write!(f, "__WRITE_LINE__"), } } @@ -302,6 +304,7 @@ pub fn lexer<'src>() -> impl Parser< let built_in_identifier = choice(( just("__READ_LINE__").to(BuiltInIdentifier::ReadLine), + just("__SLEEP__").to(BuiltInIdentifier::Sleep), just("__WRITE_LINE__").to(BuiltInIdentifier::WriteLine), )) .map(Token::BuiltInIdentifier); diff --git a/dust-lang/src/parser.rs b/dust-lang/src/parser.rs index 81c3f59..86eb7b3 100644 --- a/dust-lang/src/parser.rs +++ b/dust-lang/src/parser.rs @@ -236,10 +236,8 @@ pub fn parser<'src>() -> impl Parser< let built_in_function = { select! { Token::BuiltInIdentifier(built_in_identifier) => { - match built_in_identifier { - BuiltInIdentifier::ReadLine => BuiltInFunction::ReadLine, - BuiltInIdentifier::WriteLine => BuiltInFunction::WriteLine, - } + match built_in_identifier {BuiltInIdentifier::ReadLine=>BuiltInFunction::ReadLine,BuiltInIdentifier::WriteLine=>BuiltInFunction::WriteLine, + BuiltInIdentifier::Sleep => BuiltInFunction::Sleep, } } } } diff --git a/dust-shell/Cargo.toml b/dust-shell/Cargo.toml index abd0fb5..e452d03 100644 --- a/dust-shell/Cargo.toml +++ b/dust-shell/Cargo.toml @@ -9,6 +9,7 @@ readme.workspace = true repository.workspace = true [dependencies] +ariadne = "0.4.0" clap = { version = "4.5.3", features = ["derive"] } colored = "2.1.0" dust-lang = { path = "../dust-lang" } diff --git a/dust-shell/src/cli.rs b/dust-shell/src/cli.rs index ee685b3..126ac27 100644 --- a/dust-shell/src/cli.rs +++ b/dust-shell/src/cli.rs @@ -5,6 +5,7 @@ use std::{ process::Command, }; +use ariadne::sources; use dust_lang::{ context::{Context, ValueData}, *, @@ -87,9 +88,11 @@ pub fn run_shell(context: Context) { Ok(None) => {} Err(errors) => { for error in errors { - let report = error.build_report(&buffer).unwrap(); + let report = error.build_report(&"input").unwrap(); - stderr().write_all(&report).unwrap(); + report + .write_for_stdout(sources([(&"input", buffer.clone())]), stderr()) + .unwrap(); } } } diff --git a/dust-shell/src/error.rs b/dust-shell/src/error.rs new file mode 100644 index 0000000..3601a59 --- /dev/null +++ b/dust-shell/src/error.rs @@ -0,0 +1,218 @@ +use ariadne::{Color, Fmt, Label, Report, ReportKind}; +use dust_lang::error::{Error as DustError, RuntimeError, TypeConflict, ValidationError}; +use std::{fmt::Debug, io, ops::Range, path::Path, rc::Rc}; + +#[derive(Debug)] +pub enum Error { + Dust { + errors: Vec, + }, +} + +impl Error { + pub fn build_reports<'id>( + self, + source_id: Rc, + source: &str, + ) -> Result, Range)>>, io::Error> { + if let Error::Dust { errors } = self { + let mut reports = Vec::new(); + + for error in errors { + let (mut builder, validation_error, error_position) = match error { + DustError::Parse { + expected, + span, + reason, + } => { + let description = if expected.is_empty() { + "Invalid token.".to_string() + } else { + format!("Expected {expected}.") + }; + + ( + Report::build( + ReportKind::Custom("Parsing Error", Color::Yellow), + source_id.clone(), + span.1, + ) + .with_message(description) + .with_label( + Label::new((source_id.clone(), span.0..span.1)) + .with_message(reason) + .with_color(Color::Red), + ), + None, + span.into(), + ) + } + DustError::Lex { + expected, + span, + reason, + } => { + let description = if expected.is_empty() { + "Invalid character.".to_string() + } else { + format!("Expected {expected}.") + }; + + ( + Report::build( + ReportKind::Custom("Lexing Error", Color::Yellow), + source_id.clone(), + span.1, + ) + .with_message(description) + .with_label( + Label::new((source_id.clone(), span.0..span.1)) + .with_message(reason) + .with_color(Color::Red), + ), + None, + span.into(), + ) + } + DustError::Runtime { error, position } => ( + Report::build( + ReportKind::Custom("Runtime Error", Color::Red), + source_id.clone(), + position.1, + ) + .with_message("An error occured that forced the program to exit.") + .with_note( + "There may be unexpected side-effects because the program could not finish.", + ) + .with_help( + "This is the interpreter's fault. Please submit a bug with this error message.", + ), + if let RuntimeError::ValidationFailure(validation_error) = error { + Some(validation_error) + } else { + None + }, + position, + ), + DustError::Validation { error, position } => ( + Report::build( + ReportKind::Custom("Validation Error", Color::Magenta), + source_id.clone(), + position.1, + ) + .with_message("The syntax is valid but this code is not sound.") + .with_note("This error was detected by the interpreter before running the code."), + Some(error), + position, + ), + }; + + let type_color = Color::Green; + let identifier_color = Color::Blue; + + if let Some(validation_error) = validation_error { + match validation_error { + ValidationError::ExpectedBoolean { actual, position } => { + builder.add_label( + Label::new((source_id.clone(), position.0..position.1)) + .with_message(format!( + "Expected {} but got {}.", + "boolean".fg(type_color), + actual.fg(type_color) + )), + ); + } + ValidationError::ExpectedIntegerOrFloat(position) => { + builder.add_label( + Label::new((source_id.clone(), position.0..position.1)) + .with_message(format!( + "Expected {} or {}.", + "integer".fg(type_color), + "float".fg(type_color) + )), + ); + } + ValidationError::RwLockPoison(_) => todo!(), + ValidationError::TypeCheck { + conflict, + actual_position, + expected_position: expected_postion, + } => { + let TypeConflict { actual, expected } = conflict; + + builder = builder.with_message("A type conflict was found."); + + builder.add_labels([ + Label::new(( + source_id.clone(), + expected_postion.0..expected_postion.1, + )) + .with_message(format!( + "Type {} established here.", + expected.fg(type_color) + )), + Label::new(( + source_id.clone(), + actual_position.0..actual_position.1, + )) + .with_message(format!("Got type {} here.", actual.fg(type_color))), + ]); + } + ValidationError::VariableNotFound(identifier) => builder.add_label( + Label::new((source_id.clone(), error_position.0..error_position.1)) + .with_message(format!( + "Variable {} does not exist in this context.", + identifier.fg(identifier_color) + )), + ), + ValidationError::CannotIndex { r#type, position } => builder.add_label( + Label::new((source_id.clone(), position.0..position.1)).with_message( + format!("Cannot index into a {}.", r#type.fg(type_color)), + ), + ), + ValidationError::CannotIndexWith { + collection_type, + collection_position, + index_type, + index_position, + } => { + builder = builder.with_message(format!( + "Cannot index into {} with {}.", + collection_type.clone().fg(type_color), + index_type.clone().fg(type_color) + )); + + builder.add_labels([ + Label::new(( + source_id.clone(), + collection_position.0..collection_position.1, + )) + .with_message(format!( + "This has type {}.", + collection_type.fg(type_color), + )), + Label::new((source_id.clone(), index_position.0..index_position.1)) + .with_message(format!( + "This has type {}.", + index_type.fg(type_color), + )), + ]) + } + ValidationError::InterpreterExpectedReturn(_) => todo!(), + ValidationError::ExpectedFunction { .. } => todo!(), + ValidationError::ExpectedValue(_) => todo!(), + ValidationError::PropertyNotFound { .. } => todo!(), + ValidationError::WrongArguments { .. } => todo!(), + } + } + let report = builder.finish(); + + reports.push(report); + } + + return Ok(reports); + } else { + return Ok(Vec::with_capacity(0)); + }; + } +} diff --git a/dust-shell/src/main.rs b/dust-shell/src/main.rs index 5282191..4555440 100644 --- a/dust-shell/src/main.rs +++ b/dust-shell/src/main.rs @@ -1,13 +1,18 @@ //! Command line interface for the dust programming language. mod cli; +mod error; +use ariadne::sources; use clap::Parser; use cli::run_shell; use colored::Colorize; +use error::Error; use std::{ fs::read_to_string, io::{stderr, Write}, + path::Path, + rc::Rc, }; use dust_lang::{context::Context, interpret}; @@ -37,11 +42,10 @@ fn main() { let args = Args::parse(); let context = Context::new(); - - let source = if let Some(path) = &args.path { - read_to_string(path).unwrap() + let (source, source_id) = if let Some(path) = args.path { + (read_to_string(&path).unwrap(), Rc::new(path)) } else if let Some(command) = args.command { - command + (command, Rc::new("input".to_string())) } else { return run_shell(context); }; @@ -55,10 +59,14 @@ fn main() { } } Err(errors) => { - for error in errors { - let report = error.build_report(&source).unwrap(); + let reports = Error::Dust { errors } + .build_reports(source_id.clone(), &source) + .unwrap(); - stderr().write_all(&report).unwrap(); + for report in reports { + report + .write_for_stdout(sources([(source_id.clone(), source.clone())]), stderr()) + .unwrap(); } } } diff --git a/std/thread.ds b/std/thread.ds new file mode 100644 index 0000000..2ef663e --- /dev/null +++ b/std/thread.ds @@ -0,0 +1,5 @@ +thread = { + sleep = (milliseconds: int) : none { + __SLEEP__() + } +}