diff --git a/Cargo.lock b/Cargo.lock index 60c57ef1..d04953ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,7 @@ dependencies = [ "itertools", "miette", "ordinal", + "owo-colors", "pretty_assertions", "strum", "thiserror", diff --git a/crates/aiken-lang/Cargo.toml b/crates/aiken-lang/Cargo.toml index 93351d58..d87f2b2e 100644 --- a/crates/aiken-lang/Cargo.toml +++ b/crates/aiken-lang/Cargo.toml @@ -13,9 +13,11 @@ authors = ["Lucas Rosa ", "Kasey White "] [dependencies] chumsky = "0.8.0" indexmap = "1.9.1" +indoc = "1.0.7" itertools = "0.10.5" miette = "5.2.0" ordinal = "0.3.2" +owo-colors = "3.5.0" strum = "0.24.1" thiserror = "1.0.37" uplc = { path = '../uplc', version = "0.0.25" } diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 795dfedf..39d1009a 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -488,7 +488,7 @@ impl<'comments> Formatter<'comments> { .group() } - fn type_arguments<'a>(&mut self, args: &'a [Annotation]) -> Document<'a> { + pub fn type_arguments<'a>(&mut self, args: &'a [Annotation]) -> Document<'a> { wrap_generics(args.iter().map(|t| self.annotation(t))) } diff --git a/crates/aiken-lang/src/parser/error.rs b/crates/aiken-lang/src/parser/error.rs index 5a83deff..16fcaa3b 100644 --- a/crates/aiken-lang/src/parser/error.rs +++ b/crates/aiken-lang/src/parser/error.rs @@ -80,21 +80,12 @@ impl> chumsky::Error for ParseError { #[derive(Debug, PartialEq, Eq, Diagnostic, thiserror::Error)] pub enum ErrorKind { - #[error("Unexpected end")] + #[error("I arrived at the end of the file unexpectedly.")] UnexpectedEnd, #[error("{0}")] #[diagnostic(help("{}", .0.help().unwrap_or_else(|| Box::new(""))))] Unexpected(Pattern), - #[error("Unclosed {start}")] - Unclosed { - start: Pattern, - #[label] - before_span: Span, - before: Option, - }, - #[error("No end branch")] - NoEndBranch, - #[error("Invalid tuple index")] + #[error("I discovered an invalid tuple index.")] #[diagnostic()] InvalidTupleIndex { #[help] @@ -104,39 +95,39 @@ pub enum ErrorKind { #[derive(Debug, PartialEq, Eq, Hash, Diagnostic, thiserror::Error)] pub enum Pattern { - #[error("Unexpected {0:?}")] - #[diagnostic(help("Try removing it"))] + #[error("I found an unexpected char '{0:?}'.")] + #[diagnostic(help("Try removing it!"))] Char(char), - #[error("Unexpected {0}")] - #[diagnostic(help("Try removing it"))] + #[error("I found an unexpected token '{0}'.")] + #[diagnostic(help("Try removing it!"))] Token(Token), - #[error("Unexpected literal")] - #[diagnostic(help("Try removing it"))] + #[error("I found an unexpected literal value.")] + #[diagnostic(help("Try removing it!"))] Literal, - #[error("Unexpected type name")] - #[diagnostic(help("Try removing it"))] + #[error("I found an unexpected type name.")] + #[diagnostic(help("Try removing it!"))] TypeIdent, - #[error("Unexpected indentifier")] - #[diagnostic(help("Try removing it"))] + #[error("I found an unexpected indentifier.")] + #[diagnostic(help("Try removing it!"))] TermIdent, - #[error("Unexpected end of input")] + #[error("I found an unexpected end of input.")] End, - #[error("Malformed list spread pattern")] - #[diagnostic(help("List spread in matches can\nuse have a discard or var"))] + #[error("I found a malformed list spread pattern.")] + #[diagnostic(help("List spread in matches can use a discard '_' or var."))] Match, - #[error("Malformed byte literal")] - #[diagnostic(help("Bytes must be between 0-255"))] + #[error("I found an out-of-bound byte literal.")] + #[diagnostic(help("Bytes must be between 0-255."))] Byte, - #[error("Unexpected pattern")] + #[error("I found an unexpected pattern.")] #[diagnostic(help( - "If no label is provided then only variables\nmatching a field name are allowed" + "If no label is provided then only variables\nmatching a field name are allowed." ))] RecordPunning, - #[error("Unexpected label")] - #[diagnostic(help("You can only use labels with curly braces"))] + #[error("I found an unexpected label.")] + #[diagnostic(help("You can only use labels surrounded by curly braces"))] Label, - #[error("Unexpected hole")] - #[diagnostic(help("You can only use capture syntax with functions not constructors"))] + #[error("I found an unexpected discard '_'.")] + #[diagnostic(help("You can only use capture syntax with functions not constructors."))] Discard, } diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 1477bf4d..0680eab3 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -146,10 +146,9 @@ impl<'a> Environment<'a> { if let Type::Fn { args, ret } = tipo.deref() { return if args.len() != arity { - Err(Error::IncorrectArity { + Err(Error::IncorrectFunctionCallArity { expected: args.len(), given: arity, - labels: vec![], location: call_location, }) } else { @@ -315,8 +314,10 @@ impl<'a> Environment<'a> { location: Span, ) -> Result<&ValueConstructor, Error> { match module { - None => self.scope.get(name).ok_or_else(|| { - Error::unknown_variable_or_type(location, name, self.local_value_names()) + None => self.scope.get(name).ok_or_else(|| Error::UnknownVariable { + location, + name: name.to_string(), + variables: self.local_value_names(), }), Some(m) => { @@ -770,13 +771,21 @@ impl<'a> Environment<'a> { }; } else if !value_imported { // Error if no type or value was found with that name - return Err(Error::unknown_module_field( - *location, - name.clone(), - module.join("/"), - module_info.values.keys().map(|t| t.to_string()).collect(), - module_info.types.keys().map(|t| t.to_string()).collect(), - )); + return Err(Error::UnknownModuleField { + location: *location, + name: name.clone(), + module_name: module.join("/"), + value_constructors: module_info + .values + .keys() + .map(|t| t.to_string()) + .collect(), + type_constructors: module_info + .types + .keys() + .map(|t| t.to_string()) + .collect(), + }); } } @@ -999,10 +1008,10 @@ impl<'a> Environment<'a> { self.ungeneralised_functions.insert(name.to_string()); // Create the field map so we can reorder labels for usage of this function - let mut field_map = FieldMap::new(args.len()); + let mut field_map = FieldMap::new(args.len(), true); for (i, arg) in args.iter().enumerate() { - field_map.insert(arg.arg_name.get_label().clone(), i, location)?; + field_map.insert(arg.arg_name.get_label().clone(), i, &arg.location)?; } let field_map = field_map.into_option(); @@ -1067,7 +1076,6 @@ impl<'a> Environment<'a> { } Definition::DataType(DataType { - location, public, opaque, name, @@ -1105,14 +1113,17 @@ impl<'a> Environment<'a> { for constructor in constructors { assert_unique_value_name(names, &constructor.name, &constructor.location)?; - let mut field_map = FieldMap::new(constructor.arguments.len()); + let mut field_map = FieldMap::new(constructor.arguments.len(), false); let mut args_types = Vec::with_capacity(constructor.arguments.len()); for ( i, RecordConstructorArg { - label, annotation, .. + label, + annotation, + location, + .. }, ) in constructor.arguments.iter().enumerate() { diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 1a898f33..51924d3f 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -1,216 +1,206 @@ -use std::{collections::HashMap, sync::Arc}; - -use ordinal::Ordinal; - -use miette::Diagnostic; - +use super::Type; use crate::{ - ast::{BinOp, CallArg, Span, TodoKind, UntypedPattern}, + ast::{Annotation, BinOp, CallArg, Span, TodoKind, UntypedPattern}, format::Formatter, levenshtein, + pretty::Documentable, }; +use indoc::formatdoc; +use miette::{Diagnostic, LabeledSpan}; +use ordinal::Ordinal; +use owo_colors::OwoColorize; +use std::{collections::HashMap, fmt::Display, sync::Arc}; -use super::Type; - -#[derive(Debug, thiserror::Error, Diagnostic)] +#[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Duplicate argument '{label}'\n")] - #[diagnostic(help("Try renaming it"))] - DuplicateArgument { - #[label] - location: Span, - label: String, - }, + #[error("I found two function arguments both called '{}'.\n", label.purple())] + DuplicateArgument { locations: Vec, label: String }, - #[error("Duplicate const '{name}'\n")] - #[diagnostic(help("Try renaming it"))] + #[error("I found two top-level constants declared with the same name: '{}'.\n", name.purple())] DuplicateConstName { - #[label] location: Span, - #[label] previous_location: Span, name: String, }, - #[error("Duplicate import '{name}'\n")] - #[diagnostic(help("Try renaming it"))] + #[error("I noticed you were importing '{}' twice.\n", name.purple())] DuplicateImport { - #[label] location: Span, - #[label] previous_location: Span, name: String, }, - #[error("Duplicate field '{label}'\n")] - #[diagnostic(help("Try renaming it"))] - DuplicateField { - #[label] - location: Span, - label: String, - }, + #[error("I stumbled upon the field '{}' twice in a data-type definition.\n", label.purple())] + DuplicateField { locations: Vec, label: String }, - #[error("Duplicate name '{name}'\n")] - #[diagnostic(help("Try renaming it"))] + #[error("I discovered two top-level objects referred to as '{}'.\n", name.purple())] DuplicateName { - #[label] location: Span, - #[label] previous_location: Span, name: String, }, - #[error("Duplicate type name '{name}'\n")] - #[diagnostic(help("Try renaming it"))] + #[error("I found two types declared with the same name: '{}'.\n", name.purple())] DuplicateTypeName { - #[label] location: Span, - #[label] previous_location: Span, name: String, }, - #[error("Incorrect arity\n\nExpected\n\n {expected}\n\nGiven\n\n {given}\n")] - IncorrectArity { - #[label] + #[error("I saw a {} fields in a context where there should be {}.\n", given.purple(), expected.purple())] + IncorrectFieldsArity { location: Span, expected: usize, given: usize, labels: Vec, }, - #[error("Incorrect number of clause patterns\n\nExpected\n\n{expected}\n\nGiven\n\n{given}\n")] - IncorrectNumClausePatterns { - #[label] + #[error("I saw a function or constructor that expects {} arguments be called with {} arguments.\n", expected.purple(), given.purple())] + IncorrectFunctionCallArity { location: Span, expected: usize, given: usize, }, - #[error("Incorrect type arity for `{name}`\n\nExpected\n\n{expected}\n\nGiven\n\n{given}\n")] + #[error("I saw a pattern on a constructor that has {} fields be matched with {} arguments.\n", expected.purple(), given.len().purple())] + IncorrectPatternArity { + location: Span, + expected: usize, + given: Vec>, + name: String, + module: Option, + is_record: bool, + }, + + // TODO: Since we do not actually support patterns on multiple items, we won't likely ever + // encounter that error. We could simplify a bit the type-checker and get rid of that error + // eventually. + #[error("I counted {} different clauses in a multi-pattern instead of {}.\n", given.purple(), expected.purple())] + IncorrectNumClausePatterns { + location: Span, + expected: usize, + given: usize, + }, + + #[error("I saw a pattern on a {}-tuple be matched into a {}-tuple.\n", expected.purple(), given.purple())] + IncorrectTupleArity { + location: Span, + expected: usize, + given: usize, + }, + + #[error("I noticed a generic data-type with {} type parameters instead of {}.\n", given.purple(), expected.purple())] IncorrectTypeArity { - #[label] location: Span, name: String, expected: usize, given: usize, }, - #[error("Non-exhaustive pattern match\n")] + #[error("I realized that a given 'when clause' is non-exhaustive.\n")] NotExhaustivePatternMatch { - #[label] location: Span, unmatched: Vec, }, - #[error("Not a function\n")] - NotFn { - #[label] - location: Span, - tipo: Arc, - }, + #[error("I tripped over a call attempt on something that isn't a function.\n")] + NotFn { location: Span, tipo: Arc }, - #[error("Module '{name}' contains the keyword '{keyword}', which is forbidden\n")] + #[error( + "I realized the module '{}' contains the keyword '{}', which is forbidden.\n", + name.purple(), + keyword.purple() + )] KeywordInModuleName { name: String, keyword: String }, - #[error("Clause guard '{name}' is not local\n")] - NonLocalClauseGuardVariable { - #[label] - location: Span, - name: String, - }, + #[error("I stumble upon an invalid (non-local) clause guard '{}'.\n", name.purple())] + NonLocalClauseGuardVariable { location: Span, name: String }, - #[error("Positional argument after labeled\n")] + #[error("I discovered a positional argument after a label argument.\n")] PositionalArgumentAfterLabeled { - #[label] location: Span, + labeled_arg_location: Span, }, - #[error("Private type leaked\n")] - PrivateTypeLeak { - #[label] - location: Span, - leaked: Type, - }, + #[error("I caught a private value trying to escape.\n")] + PrivateTypeLeak { location: Span, leaked: Type }, - #[error("Record access unknown type\n")] - RecordAccessUnknownType { - #[label] - location: Span, - }, + #[error("I couldn't figure out the type of a record you're trying to access.\n")] + RecordAccessUnknownType { location: Span }, - #[error("Record update invalid constructor\n")] - RecordUpdateInvalidConstructor { - #[label] - location: Span, - }, + #[error("I tripped over an invalid constructor in a record update.\n")] + RecordUpdateInvalidConstructor { location: Span }, - #[error("{name} is a reserved module name\n")] + #[error("I realized you used '{}' as a module name, which is reserved (and not available).\n", name.purple())] ReservedModuleName { name: String }, - #[error("Unexpected labeled argument '{label}'\n")] - #[diagnostic()] - UnexpectedLabeledArg { - #[label] + #[error("I tripped over the following labeled argument: {}.\n", label.purple())] + UnexpectedLabeledArg { location: Span, label: String }, + + #[error("I tripped over the following labeled argument: {}.\n", label.purple())] + UnexpectedLabeledArgInPattern { location: Span, label: String, - #[help] - hint: Option, + name: String, + args: Vec>, + module: Option, + with_spread: bool, }, - #[error("Unexpected type hole\n")] - UnexpectedTypeHole { - #[label] - location: Span, - }, + // TODO: Seems like we can't really trigger this error because we allow type holes everywhere + // anyway. We need to revise that perhaps. + #[error("I stumbled upon an unexpected type hole.\n")] + UnexpectedTypeHole { location: Span }, - #[error("Unknown labels\n")] + #[error("I tripped over some unknown labels in a pattern or function.\n")] UnknownLabels { unknown: Vec<(String, Span)>, valid: Vec, supplied: Vec, }, - #[error("Unknown module '{name}'\n")] + #[error("I stumble upon a reference to an unknown module: '{}'\n", name.purple())] UnknownModule { - #[label] location: Span, name: String, imported_modules: Vec, }, - #[error("Unknown import '{name}' from module '{module_name}'\n")] - #[diagnostic()] + #[error( + "I found an unknown import '{}' from module '{}'\n", + name.purple(), + module_name.purple() + )] UnknownModuleField { - #[label] location: Span, name: String, module_name: String, - #[help] - hint: Option, + value_constructors: Vec, + type_constructors: Vec, }, - #[error("Unknown module value '{name}'\n")] + #[error("I tried to find '{}' in '{}' but didn't.\n", name.purple(), module_name.purple())] UnknownModuleValue { - #[label] location: Span, name: String, module_name: String, value_constructors: Vec, }, - #[error("Unknown type '{name}' in module '{module_name}'\n")] + #[error("I tried to find '{}' in '{}' but didn't.\n", name.purple(), module_name.purple())] UnknownModuleType { - #[label] location: Span, name: String, module_name: String, type_constructors: Vec, }, - #[error("Unknown record field\n\n{label}\n")] + #[error( + "I tried to find the field '{}' in a record of type '{}' but didn't.\n", + label.purple(), + typ.to_pretty(4).purple() + )] UnknownRecordField { - #[label] location: Span, typ: Arc, label: String, @@ -218,54 +208,35 @@ pub enum Error { situation: Option, }, - #[error("Unknown type '{name}'\n")] + #[error("I found a reference to an unknown type: '{}'.\n", name.purple())] UnknownType { - #[label] location: Span, name: String, types: Vec, }, - #[error("Unknown variable '{name}'\n")] - #[diagnostic()] + #[error("I found a reference to an unknown variable: '{}'.\n", name.purple())] UnknownVariable { - #[label] location: Span, name: String, - #[help] - hint: String, + variables: Vec, }, - #[error("Unknown data-type constructor '{name}'\n")] - #[diagnostic()] + #[error("I found a reference to an unknown data-type constructor: '{}'.\n", name.purple())] UnknownTypeConstructor { - #[label] location: Span, name: String, - #[help] - hint: String, + constructors: Vec, }, - #[error("Unnecessary spread operator\n")] - UnnecessarySpreadOperator { - #[label] - location: Span, - arity: usize, - }, + #[error("I discovered a redundant spread operator.\n")] + UnnecessarySpreadOperator { location: Span, arity: usize }, - #[error("Cannot update a type with multiple constructors\n")] - UpdateMultiConstructorType { - #[label] - location: Span, - }, + #[error("I tripped over a record-update on a data-type with more than one constructor.\n")] + UpdateMultiConstructorType { location: Span }, - #[error( - "Type Mismatch\n\nExpected type:\n\n{}\n\nFound type:\n\n{}\n", - expected.to_pretty_with_names(rigid_type_names.clone(), 4), - given.to_pretty_with_names(rigid_type_names.clone(), 4) - )] + #[error("I struggled to unify the types of two expressions.\n")] CouldNotUnify { - #[label] location: Span, expected: Arc, given: Arc, @@ -273,49 +244,32 @@ pub enum Error { rigid_type_names: HashMap, }, - #[error("")] - ExtraVarInAlternativePattern { - #[label] - location: Span, - name: String, - }, + #[error("I tripped over an extra variable in an alternative pattern: {}.\n", name.purple())] + ExtraVarInAlternativePattern { location: Span, name: String }, - #[error("")] - MissingVarInAlternativePattern { - #[label] - location: Span, - name: String, - }, + #[error("I found a missing variable in an alternative pattern: {}.\n", name.purple())] + MissingVarInAlternativePattern { location: Span, name: String }, - #[error("")] - DuplicateVarInPattern { - #[label] - location: Span, - name: String, - }, + #[error("I realized the variable '{}' was mentioned more than once in an alternative pattern.\n ", name.purple())] + DuplicateVarInPattern { location: Span, name: String }, - #[error("Cyclic type definition detected: {}\n", types.join(" -> "))] - CyclicTypeDefinitions { - #[label] - location: Span, - types: Vec, - }, + #[error("I almost got caught in an infinite cycle of type definitions: {}.\n", types.join(" -> "))] + CyclicTypeDefinitions { location: Span, types: Vec }, - #[error("Recursive type detected\n")] - RecursiveType { - #[label] - location: Span, - }, + #[error("I almost got caught in an endless loop while inferring a recursive type.\n")] + RecursiveType { location: Span }, - #[error("Trying to access tuple elements on something else than a tuple\n")] - NotATuple { - #[label] - location: Span, - }, + #[error( + "I tripped over an attempt to access tuple elements on something else than a tuple.\n" + )] + NotATuple { location: Span, tipo: Arc }, - #[error("Trying to access the {} element of a {}-tuple\n", Ordinal(*index + 1).to_string(), size)] + #[error( + "I discovered an attempt to access the {} element of a {}-tuple.\n", + Ordinal(*index + 1).to_string().purple(), + size.purple() + )] TupleIndexOutOfBound { - #[label] location: Span, index: usize, size: usize, @@ -356,14 +310,6 @@ impl Error { } } - pub fn inconsistent_try(self, return_value_is_result: bool) -> Self { - self.with_unify_error_situation(if return_value_is_result { - UnifyErrorSituation::TryErrorMismatch - } else { - UnifyErrorSituation::TryReturnResult - }) - } - pub fn operator_situation(self, binop: BinOp) -> Self { self.with_unify_error_situation(UnifyErrorSituation::Operator(binop)) } @@ -403,136 +349,916 @@ impl Error { other => other, } } +} - pub fn unknown_module_field( - location: Span, - name: String, - module_name: String, - value_constructors: Vec, - type_constructors: Vec, - ) -> Self { - let mut candidates = vec![]; - candidates.extend(value_constructors); - candidates.extend(type_constructors); +impl Diagnostic for Error { + fn severity(&self) -> Option { + Some(miette::Severity::Error) + } - let hint = candidates - .iter() - .map(|s| (s, levenshtein::distance(&name, s))) - .min_by(|(_, a), (_, b)| a.cmp(b)) - .and_then(|(suggestion, distance)| { - if distance <= 5 { - Some(format!("Did you mean to import '{suggestion}'?")) - } else { - None - } - }); - - Self::UnknownModuleField { - location, - name, - module_name, - hint, + // Unique diagnostic code that can be used to look up more information about this Diagnostic. Ideally also globally unique, and documented in the toplevel crate’s documentation for easy searching. Rust path format (foo::bar::baz) is recommended, but more classic codes like E0123 or enums will work just fine. + fn code<'a>(&'a self) -> Option> { + match self { + Self::DuplicateArgument { .. } => Some(Box::new("duplicate_argument")), + Self::DuplicateConstName { .. } => Some(Box::new("duplicate_const_name")), + Self::DuplicateImport { .. } => Some(Box::new("duplicate_import")), + Self::DuplicateField { .. } => Some(Box::new("duplicate_field")), + Self::DuplicateName { .. } => Some(Box::new("duplicate_name")), + Self::DuplicateTypeName { .. } => Some(Box::new("duplicate_type_name")), + Self::IncorrectFieldsArity { .. } => Some(Box::new("incorrect_fields_arity")), + Self::IncorrectFunctionCallArity { .. } => Some(Box::new("incorrect_fn_arity")), + Self::IncorrectPatternArity { .. } => Some(Box::new("incorrect_pattern_arity")), + Self::IncorrectNumClausePatterns { .. } => { + Some(Box::new("incorrect_num_clause_patterns")) + } + Self::IncorrectTupleArity { .. } => Some(Box::new("incorrect_tuple_arity")), + Self::IncorrectTypeArity { .. } => Some(Box::new("incorrect_type_arity")), + Self::NotExhaustivePatternMatch { .. } => { + Some(Box::new("non_exhaustive_pattern_match")) + } + Self::NotFn { .. } => Some(Box::new("not_fn")), + Self::KeywordInModuleName { .. } => Some(Box::new("keyword_in_module_name")), + Self::NonLocalClauseGuardVariable { .. } => { + Some(Box::new("non_local_clause_guard_variable")) + } + Self::PositionalArgumentAfterLabeled { .. } => { + Some(Box::new("positional_argument_after_labeled")) + } + Self::PrivateTypeLeak { .. } => Some(Box::new("private_type_leak")), + Self::RecordAccessUnknownType { .. } => Some(Box::new("record_access_unknown_type")), + Self::RecordUpdateInvalidConstructor { .. } => { + Some(Box::new("record_update_invalid_constructor")) + } + Self::ReservedModuleName { .. } => Some(Box::new("reserved_module_name")), + Self::UnexpectedLabeledArg { .. } => Some(Box::new("unexpected_labeled_arg")), + Self::UnexpectedLabeledArgInPattern { .. } => { + Some(Box::new("unexpected_labeled_arg_in_pattern")) + } + Self::UnexpectedTypeHole { .. } => Some(Box::new("unexpected_type_hole")), + Self::UnknownLabels { .. } => Some(Box::new("unknown_labels")), + Self::UnknownModule { .. } => Some(Box::new("unknown_module")), + Self::UnknownModuleField { .. } => Some(Box::new("unknown_module_field")), + Self::UnknownModuleValue { .. } => Some(Box::new("unknown_module_value")), + Self::UnknownModuleType { .. } => Some(Box::new("unknown_module_type")), + Self::UnknownRecordField { .. } => Some(Box::new("unknown_record_field")), + Self::UnknownType { .. } => Some(Box::new("unknown_type")), + Self::UnknownVariable { .. } => Some(Box::new("unknown_variable")), + Self::UnknownTypeConstructor { .. } => Some(Box::new("unknown_type_constructor")), + Self::UnnecessarySpreadOperator { .. } => Some(Box::new("unnecessary_spread_operator")), + Self::UpdateMultiConstructorType { .. } => { + Some(Box::new("update_multi_constructor_type")) + } + Self::CouldNotUnify { .. } => Some(Box::new("could_not_unify")), + Self::ExtraVarInAlternativePattern { .. } => { + Some(Box::new("extra_var_in_alternative_pattern")) + } + Self::MissingVarInAlternativePattern { .. } => { + Some(Box::new("missing_var_in_alternative_pattern")) + } + Self::DuplicateVarInPattern { .. } => Some(Box::new("duplicate_var_in_pattern")), + Self::CyclicTypeDefinitions { .. } => Some(Box::new("cyclic_type_definitions")), + Self::RecursiveType { .. } => Some(Box::new("recursive_type")), + Self::NotATuple { .. } => Some(Box::new("not_a_tuple")), + Self::TupleIndexOutOfBound { .. } => Some(Box::new("tuple_index_out_of_bound")), } } - pub fn unknown_variable_or_type(location: Span, name: &str, variables: Vec) -> Self { - let hint = variables - .iter() - .map(|s| (s, levenshtein::distance(name, s))) - .min_by(|(_, a), (_, b)| a.cmp(b)) - .and_then(|(suggestion, distance)| { - if distance <= 3 { - Some(format!("Did you mean '{suggestion}'?")) + // Additional help text related to this Diagnostic. Do you have any advice for the poor soul who’s just run into this issue? + fn help<'a>(&'a self) -> Option> { + match self { + Self::DuplicateArgument { .. } => { + // TODO: Suggest names based on types when the duplicate argument is `_` + Some(Box::new(formatdoc! { + r#"Function arguments cannot have the same name. You can use '_' and + numbers to distinguish between similar names. + "# + })) + }, + + Self::DuplicateConstName { .. } => Some(Box::new(formatdoc! { + r#"Top-level constants of a same module cannot have the same name. + You can use '_' and numbers to distinguish between similar names. + "# + })), + + Self::DuplicateImport { .. } => Some(Box::new(formatdoc! { + r#"The best thing to do from here is to remove one of them. + "# + })), + + Self::DuplicateField { .. } => Some(Box::new(formatdoc! { + r#"Data-types must have fields with strictly different names. You can + use '_' and numbers to distinguish between similar names. + + Note that it is also possible to declare data-types with positional + (nameless) fields only. For example: + + ┍━━━━━━━━━━━━━━━━━━━━━━━ + │ pub type Point {{ + │ Point(Int, Int, Int) + │ }} + "# + })), + + Self::DuplicateName { .. } => Some(Box::new(formatdoc! { + r#"Top-level definitions cannot have the same name, even if they + refer to objects with different natures (e.g. function and test). + + You can use '_' and numbers to distinguish between similar names. + "# + })), + + Self::DuplicateTypeName { .. } => Some(Box::new(formatdoc! { + r#"Types cannot have the same top-level name. You {} use '_' in + types name, but you can use numbers to distinguish between similar + names. + "#, "cannot".red() + })), + + Self::IncorrectFieldsArity { .. } => None, + + Self::IncorrectFunctionCallArity { expected, .. } => Some(Box::new(formatdoc! { + r#"Functions (and constructors) must always be called with all their + arguments (comma-separated, between brackets). + + Here, the function or constructor needs {} arguments. + + Note that Aiken supports argument capturing using '_' as placeholder + for arguments that aren't yet defined. This is like currying in some + other languages. + + For example, imagine the following function: + + ┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ fn add(x :Int, y: Int) -> Int + + From there, you can define 'increment', a function that takes a + single argument and adds one to it, as such: + + ┍━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ let increment = add(1, _) + "#, + expected.purple() + })), + + Self::IncorrectPatternArity { + name, + given, + expected, + module, + is_record, + .. + } => { + let pattern = Formatter::new() + .pattern_constructor(name, given, module, true, *is_record) + .to_pretty_string(70); + + let suggestion = if expected > &given.len() { + formatdoc! { + r#" Perhaps, try the following: + + ╰─▶ {pattern} + "# + } } else { - None + String::new() + }; + + Some(Box::new(formatdoc! { + r#"When pattern-matching on constructors, you must either match the + exact number of fields, or use the spread operator. Note that unused + fields must be discarded by prefixing their name with '_'.{suggestion} + "# + })) + }, + + Self::IncorrectNumClausePatterns { .. } => None, + + Self::IncorrectTupleArity { .. } => Some(Box::new(formatdoc! { + r#"When pattern matching on a tuple, you must match all + of its elements. Note that unused fields must be + discarded by prefixing their name with '_'. + "# + })), + + Self::IncorrectTypeArity { expected, name, .. } => { + let mut args = vec![]; + for i in 0..*expected { + args.push(Annotation::Var { + name: char::from_u32(97 + i as u32).unwrap_or('?').to_string(), + location: Span::empty(), + }); } - }); - if name.chars().into_iter().next().unwrap().is_uppercase() { - let hint = hint.unwrap_or_else(|| { - r#"Did you forget to import it? + let suggestion = name + .to_doc() + .append(Formatter::new().type_arguments(&args)) + .to_pretty_string(70); -Data-type constructors are not automatically imported, even if their type -is imported. So, if a module `aiken/pet` defines the following type: + Some(Box::new(formatdoc! { + r#"Data-types that are generic in one or more types must be written + with all their generic types in type annotations. - ┍━ aiken/pet.ak ━━━━━━━━ - │ pub type Pet {{ - │ Cat - │ Dog - │ }} + Generic types must be indicated between chevrons '<' and '>'. -You must import its constructors explicitly to use them, or prefix them -with the module's name. + Perhaps, try the following: - ┍━ foo.ak ━━━━━━━━ - │ use aiken/pet.{{Pet, Dog}} - │ - │ fn foo(pet : Pet) {{ - │ when pet is {{ - │ pet.Cat -> // ... - │ Dog -> // ... - │ }} - │ }}"# - .to_string() - }); + ╰─▶ {suggestion} + "# + })) + }, + + Self::NotExhaustivePatternMatch { unmatched, .. } => { + let missing = unmatched + .iter() + .map(|s| format!("─▶ {s}")) + .collect::>() + .join("\n"); + + Some(Box::new(formatdoc! { + r#"When clauses must be exhaustive -- that is, they must cover all possible + cases of the type they match. While it is recommended to have an explicit + branch for each constructor, you can also use the wildcard '_' as a last + branch to match any remaining result. + + In this particular instance, the following cases are missing: + + {missing} + "# + })) + }, + + Self::NotFn { tipo, .. } => { + let inference = tipo.to_pretty(4); + Some(Box::new(formatdoc! { + r#"It seems like you're trying to call something that isn't a function. + I am inferring the following type: + + ╰─▶ {inference} + "# + })) + }, + + Self::KeywordInModuleName { .. } => Some(Box::new(formatdoc! { + r#"You cannot use keywords as part of a module path name. As a quick + reminder, here's a list of all the keywords (and thus, of invalid + module path names): + + as, assert, check, const, else, fn, if, is, let, opaque, pub, test, + todo, trace, type, use, when, + "# + })), + + Self::NonLocalClauseGuardVariable { .. } => Some(Box::new(formatdoc! { + r#"There are some conditions regarding what can be used in a guard. + Values must be either local to the function, or defined as module + constants. You can't use functions or records in there. + "# + })), + + Self::PositionalArgumentAfterLabeled { .. } => Some(Box::new(formatdoc! { + r#"You can mix positional and labeled arguments, but you must put all + positional arguments (i.e. without label) at the front. + + To fix this, you'll need to either turn that argument as a labeled + argument, or make the next one positional. + "# + })), + + Self::PrivateTypeLeak { leaked, .. } => Some(Box::new(formatdoc! { + r#"I found a public value that is making use of a private type. + This would prevent other modules from actually using that value + because they wouldn't know what this type refer to. + + The culprit is: + + {type_info} + + Maybe you meant to turn it public using the 'pub' keyword? + "# + , type_info = leaked.to_pretty(4) })), + + Self::RecordAccessUnknownType { .. } => Some(Box::new(formatdoc! { + r#"I do my best to infer types of any expression; yet sometimes I + need help (don't we all?). + + Take for example the following expression: + + ┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ let foo = fn(x) {{ x.transaction }} + + At this stage, I can't quite figure out whether 'x' has indeed a + field 'transaction', because I don't know what the type of 'x' is. + You can help me by providing a type-annotation for 'x', as such: + + ┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ let foo = fn(x: ScriptContext) {{ x.transaction }} + "# + })), + + // TODO: Come back to this one once we support record updates. + Self::RecordUpdateInvalidConstructor { .. } => None, + + Self::ReservedModuleName { .. } => Some(Box::new(formatdoc! { + r#"Some module names are reserved for internal use. This the case + of: + + - aiken: where the prelude is located; + - aiken/builtin: where I store low-level Plutus builtins. + + Note that 'aiken' is also imported by default; but you can refer to + it explicitly to disambiguate with a local value that would clash + with one from that module. + "# + })), + + Self::UnexpectedLabeledArg { .. } => None, + + Self::UnexpectedLabeledArgInPattern { + args, + module, + name, + with_spread, + .. + } => { + let fixed_args = args + .iter() + .map(|arg| CallArg { + label: None, + location: arg.location, + value: arg.value.clone(), + }) + .collect::>(); + + let suggestion = Formatter::new() + .pattern_constructor(name, &fixed_args, module, *with_spread, false) + .to_pretty_string(70); + + Some(Box::new(formatdoc! { + r#"The constructor '{name}' does not have any labeled field. Its + fields must therefore be matched only by position. + + Perhaps, try the following: + + ╰─▶ {suggestion} + "# + })) + }, + + Self::UnexpectedTypeHole { .. } => None, + + Self::UnknownLabels { valid, .. } => { + let known_labels = valid + .iter() + .map(|s| format!("─▶ {s}")) + .collect::>() + .join("\n"); + Some(Box::new(formatdoc! { + r#"I don't know some of the labels used in this expression. I've + highlighted them just above. + + Here's a list of all the (valid) labels that I know of: + + {known_labels} + "# + })) + }, + + Self::UnknownModule { + name, + imported_modules, + .. + } => Some( + imported_modules + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| Box::new("Did you forget to import it?".to_string())), + ), + + Self::UnknownModuleField { + name, + value_constructors, + type_constructors, + .. + } => { + let mut candidates = vec![]; + candidates.extend(value_constructors); + candidates.extend(type_constructors); + Some( + candidates + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean to import '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| { + Box::new(formatdoc! { + r#"Did you forget to make this value public? + + Values from module must be exported using the keyword 'pub' in order to + be available from other modules. + + For example: + + ┍━ aiken/foo.ak ━━━━━━━━ + │ fn foo () {{ "foo" }} + │ + │ pub type Bar {{ + │ Bar + │ }} + + The function 'foo' is private and can't be accessed from outside of the + 'aiken/foo' module. But the data-type 'Bar' is public and available. + "# + }) + }), + ) + }, + + Self::UnknownModuleValue { + name, + value_constructors, + .. + } => Some( + value_constructors + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| { + Box::new(formatdoc! { + r#"Did you forget to make this value public? + + Values from module must be exported using the keyword 'pub' in order to + be available from other modules. + + For example: + + ┍━ aiken/foo.ak ━━━━━━━━ + │ fn foo () {{ "foo" }} + │ + │ pub type Bar {{ + │ Bar + │ }} + + The function 'foo' is private and can't be accessed from outside of the + 'aiken/foo' module. But the data-type 'Bar' is public and available. + "# + }) + }), + ), + + Self::UnknownModuleType { + name, + type_constructors, + .. + } => Some( + type_constructors + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| { + Box::new(formatdoc! { + r#"Did you forget to make this value public? + + Values from module must be exported using the keyword 'pub' in order to + be available from other modules. + + For example: + + ┍━ aiken/foo.ak ━━━━━━━━ + │ fn foo () {{ "foo" }} + │ + │ pub type Bar {{ + │ Bar + │ }} + + The function 'foo' is private and can't be accessed from outside of the + 'aiken/foo' module. But the data-type 'Bar' is public and available. + "# + }) + }) + ), + + Self::UnknownRecordField { label, fields, .. } => Some(fields + .iter() + .map(|s| (s, levenshtein::distance(label, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| Box::new("Did you forget to make the field public?".to_string())) + ), + + Self::UnknownType { name, types, .. } => Some( + types + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| Box::new("Did you forget to import it?".to_string())), + ), - Self::UnknownTypeConstructor { - name: name.to_string(), - hint, - location, - } - } else { - let hint = hint.unwrap_or_else(|| "Did you forget to import it?".to_string()); Self::UnknownVariable { - name: name.to_string(), - location, - hint, + name, variables, .. + } => { + let hint = variables + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }); + + if name.chars().into_iter().next().unwrap().is_uppercase() { + Some(hint.unwrap_or_else(|| + Box::new(formatdoc! { + r#"Did you forget to import it? + + Data-type constructors are not automatically imported, even if their type + is imported. So, if a module 'aiken/pet' defines the following type: + + ┍━ aiken/pet.ak ━━━━━━━━ + │ pub type Pet {{ + │ Cat + │ Dog + │ }} + + You must import its constructors explicitly to use them, or prefix them + with the module's name. + + ┍━ foo.ak ━━━━━━━━ + │ use aiken/pet.{{Pet, Dog}} + │ + │ fn foo(pet : Pet) {{ + │ when pet is {{ + │ pet.Cat -> // ... + │ Dog -> // ... + │ }} + │ }}" + "# + }) + )) + } else { + Some(hint.unwrap_or_else(|| Box::new("Did you forget to import it?".to_string()))) + } } + + Self::UnknownTypeConstructor { name, constructors, .. } => Some( + constructors + .iter() + .map(|s| (s, levenshtein::distance(name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 4 { + Some(Box::new(format!("Did you mean '{suggestion}'?"))) + } else { + None + } + }) + .unwrap_or_else(|| Box::new("Did you forget to import it?".to_string())) + ), + + Self::UnnecessarySpreadOperator { arity, .. } => Some(Box::new(formatdoc! { + r#"The spread operator comes in handy when matching on some fields of + a constructor. However, here you've matched all {arity} fields of the + constructor which makes the spread operator redundant. + + The best thing to do from here is to remove it. + "# }) + ), + + // TODO: Come and refine this once we support record updates. + Self::UpdateMultiConstructorType {..} => None, + + Self::CouldNotUnify { expected, given, situation, rigid_type_names, .. } => { + let expected = expected.to_pretty_with_names(rigid_type_names.clone(), 4); + let given = given.to_pretty_with_names(rigid_type_names.clone(), 4); + Some(Box::new( + match situation { + Some(UnifyErrorSituation::CaseClauseMismatch) => formatdoc! { + r#"While comparing branches from a 'when/is' expression, I realized + not all branches have the same type. + + I am expecting all of them to have the following type: + + {} + + but I found some with type: + + {} + + Note that I infer the type of the entire 'when/is' expression + based on the type of the first branch I encounter. + "#, + expected.green(), + given.red() + }, + Some(UnifyErrorSituation::ReturnAnnotationMismatch) => formatdoc! { + r#"While comparing the return annotation of a function with + its actual return type, I realized that both don't match. + + I am inferring the function should return: + + {} + + but I found that it returns: + + {} + + Either, fix the annotation or adjust the function body to + return the expected type. + "#, + expected.green(), + given.red() + }, + Some(UnifyErrorSituation::PipeTypeMismatch) => formatdoc! { + r#"As I was looking at a pipeline you have defined, I realized + that one of the pipe isn't valid. + + I am expecting the pipe to send into something of type: + + {} + + but it is typed: + + {} + + Either, fix the input or change the target so that both match. + "#, + expected.green(), + given.red() + }, + Some(UnifyErrorSituation::Operator(op)) => formatdoc! { + r#"While checking operands of a binary operator, I realized + that at least one of them doesn't have the expected type. + + The '{}' operator expects operands of type: + + {} + + but I discovered the following instead: + + {} + "#, + op.to_doc().to_pretty_string(70), + expected.green(), + given.red() + }, + None => formatdoc! { + r#"I am inferring the following type: + + {} + + but I found an expression with a different type: + + {} + + Either, add type-annotation to improve my inference, or + adjust the expression to have the expected type. + "#, + expected.green(), + given.red() + }, + } + )) + }, + + Self::ExtraVarInAlternativePattern { .. } => None, + + Self::MissingVarInAlternativePattern { .. } => None, + + Self::DuplicateVarInPattern { .. } => None, + + Self::CyclicTypeDefinitions { .. } => None, + + Self::RecursiveType { .. } => Some(Box::new(formatdoc! { + r#"I have several aptitudes, but inferring recursive types isn't one them. + It is still possible to define recursive types just fine, but I will + need a little help in the form of type annotation to infer their types + should they show up. + "# + })), + + Self::NotATuple { tipo, .. } => Some(Box::new(formatdoc! { + r#"Because you used a tuple-index on an element, I assumed it had to + be a tuple or some kind, but instead I found: + + {type_info} + "#, + type_info = tipo.to_pretty(4) + })), + + Self::TupleIndexOutOfBound { .. } => None, } } - pub fn unexpected_labeled_arg<'a>(location: Span, label: String) -> Self { - Self::UnexpectedLabeledArg { - location, - label, - hint: None, - } - } - - pub fn unexpected_labeled_arg_in_pattern( - location: Span, - label: String, - name: &str, - args: &[CallArg], - module: &Option, - with_spread: bool, - ) -> Self { - let fixed_args = args - .iter() - .map(|arg| CallArg { - label: None, - location: arg.location, - value: arg.value.clone(), - }) - .collect::>(); - - let suggestion = Formatter::new() - .pattern_constructor(name, &fixed_args, module, with_spread, false) - .to_pretty_string(70); - - let hint = format!( - r#"The constructor '{name}' does not have any labeled field. Its fields -must therefore be matched only by position. - -Perhaps, try the following: - -╰─▶ {suggestion}"# - ); - - Self::UnexpectedLabeledArg { - location, - label: label.to_string(), - hint: Some(hint), + // Labels to apply to this Diagnostic’s Diagnostic::source_code + fn labels(&self) -> Option + '_>> { + match self { + Self::DuplicateArgument { locations, .. } => Some(Box::new( + locations + .into_iter() + .map(|l| LabeledSpan::new_with_span(None, *l)), + )), + Self::DuplicateConstName { + location, + previous_location, + .. + } => Some(Box::new( + vec![ + LabeledSpan::new_with_span(None, *location), + LabeledSpan::new_with_span(None, *previous_location), + ] + .into_iter(), + )), + Self::DuplicateImport { + location, + previous_location, + .. + } => Some(Box::new( + vec![ + LabeledSpan::new_with_span(None, *location), + LabeledSpan::new_with_span(None, *previous_location), + ] + .into_iter(), + )), + Self::DuplicateField { locations, .. } => Some(Box::new( + locations + .into_iter() + .map(|l| LabeledSpan::new_with_span(None, *l)), + )), + Self::DuplicateName { + location, + previous_location, + .. + } => Some(Box::new( + vec![ + LabeledSpan::new_with_span(None, *location), + LabeledSpan::new_with_span(None, *previous_location), + ] + .into_iter(), + )), + Self::DuplicateTypeName { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::IncorrectFieldsArity { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::IncorrectFunctionCallArity { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::IncorrectPatternArity { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::IncorrectNumClausePatterns { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::IncorrectTupleArity { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::IncorrectTypeArity { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::NotExhaustivePatternMatch { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::NotFn { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::KeywordInModuleName { .. } => None, + Self::NonLocalClauseGuardVariable { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::PositionalArgumentAfterLabeled { + location, + labeled_arg_location, + .. + } => Some(Box::new( + vec![ + LabeledSpan::new_with_span(Some("positional".to_string()), *location), + LabeledSpan::new_with_span(Some("labeled".to_string()), *labeled_arg_location), + ] + .into_iter(), + )), + Self::PrivateTypeLeak { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::RecordAccessUnknownType { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::RecordUpdateInvalidConstructor { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::ReservedModuleName { .. } => None, + Self::UnexpectedLabeledArg { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnexpectedLabeledArgInPattern { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnexpectedTypeHole { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownLabels { unknown, .. } => { + Some(Box::new(unknown.iter().map(|(lbl, span)| { + LabeledSpan::new_with_span(Some(lbl.clone()), *span) + }))) + } + Self::UnknownModule { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownModuleField { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownModuleValue { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownModuleType { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownRecordField { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownType { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownVariable { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnknownTypeConstructor { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UnnecessarySpreadOperator { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::UpdateMultiConstructorType { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::CouldNotUnify { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::ExtraVarInAlternativePattern { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::MissingVarInAlternativePattern { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::DuplicateVarInPattern { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::CyclicTypeDefinitions { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::RecursiveType { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::NotATuple { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), + Self::TupleIndexOutOfBound { location, .. } => Some(Box::new( + vec![LabeledSpan::new_with_span(None, *location)].into_iter(), + )), } } } @@ -636,12 +1362,6 @@ pub enum UnifyErrorSituation { /// The operands of a binary operator were incorrect. Operator(BinOp), - - /// A try expression returned a different error type to the previous try. - TryErrorMismatch, - - /// The final value of a try expression was not a Result. - TryReturnResult, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 63088090..31b74aa7 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -112,7 +112,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { // The fun has no field map and so we error if arguments have been labelled None => assert_no_labeled_arguments(&args) - .map(|(location, label)| Err(Error::unexpected_labeled_arg(location, label))) + .map(|(location, label)| Err(Error::UnexpectedLabeledArg { location, label })) .unwrap_or(Ok(()))?, } @@ -501,7 +501,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { None => { panic!("Failed to lookup record field after successfully inferring that field",) } - Some(p) => arguments.push(TypedRecordUpdateArg { + Some((p, _)) => arguments.push(TypedRecordUpdateArg { location, label: label.to_string(), value, @@ -1354,7 +1354,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { // The fun has no field map and so we error if arguments have been labelled None => assert_no_labeled_arguments(&args) .map(|(location, label)| { - Err(Error::unexpected_labeled_arg(location, label)) + Err(Error::UnexpectedLabeledArg { location, label }) }) .unwrap_or(Ok(()))?, } @@ -1729,7 +1729,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { Ok(elems[index].clone()) } } - _ => Err(Error::NotATuple { location }), + _ => Err(Error::NotATuple { + location, + tipo: tuple.tipo(), + }), }?; Ok(TypedExpr::TupleIndex { @@ -1787,12 +1790,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { self.environment .get_variable(name) .cloned() - .ok_or_else(|| { - Error::unknown_variable_or_type( - *location, - name, - self.environment.local_value_names(), - ) + .ok_or_else(|| Error::UnknownVariable { + location: *location, + name: name.to_string(), + variables: self.environment.local_value_names(), })?; // Note whether we are using an ungeneralised function so that we can diff --git a/crates/aiken-lang/src/tipo/fields.rs b/crates/aiken-lang/src/tipo/fields.rs index 2eb993e4..f1b8c4c8 100644 --- a/crates/aiken-lang/src/tipo/fields.rs +++ b/crates/aiken-lang/src/tipo/fields.rs @@ -8,23 +8,34 @@ use crate::ast::{CallArg, Span}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldMap { pub arity: usize, - pub fields: HashMap, + pub fields: HashMap, + pub is_function: bool, } impl FieldMap { - pub fn new(arity: usize) -> Self { + pub fn new(arity: usize, is_function: bool) -> Self { Self { arity, fields: HashMap::new(), + is_function, } } pub fn insert(&mut self, label: String, index: usize, location: &Span) -> Result<(), Error> { - match self.fields.insert(label.clone(), index) { - Some(_) => Err(Error::DuplicateField { - label, - location: *location, - }), + match self.fields.insert(label.clone(), (index, *location)) { + Some((_, location_other)) => { + if self.is_function { + Err(Error::DuplicateArgument { + label, + locations: vec![*location, location_other], + }) + } else { + Err(Error::DuplicateField { + label, + locations: vec![*location, location_other], + }) + } + } None => Ok(()), } } @@ -40,12 +51,12 @@ impl FieldMap { /// Reorder an argument list so that labelled fields supplied out-of-order are /// in the correct order. pub fn reorder(&self, args: &mut [CallArg], location: Span) -> Result<(), Error> { - let mut labeled_arguments_given = false; + let mut last_labeled_arguments_given: Option<&CallArg> = None; let mut seen_labels = std::collections::HashSet::new(); let mut unknown_labels = Vec::new(); if self.arity != args.len() { - return Err(Error::IncorrectArity { + return Err(Error::IncorrectFieldsArity { labels: self.incorrect_arity_labels(args), location, expected: self.arity, @@ -56,13 +67,14 @@ impl FieldMap { for arg in args.iter() { match &arg.label { Some(_) => { - labeled_arguments_given = true; + last_labeled_arguments_given = Some(arg); } None => { - if labeled_arguments_given { + if let Some(label) = last_labeled_arguments_given { return Err(Error::PositionalArgumentAfterLabeled { location: arg.location, + labeled_arg_location: label.location, }); } } @@ -90,7 +102,7 @@ impl FieldMap { } }; - let position = match self.fields.get(label) { + let (position, other_location) = match self.fields.get(label) { None => { unknown_labels.push((label.clone(), location)); @@ -110,7 +122,7 @@ impl FieldMap { } else { if seen_labels.contains(label) { return Err(Error::DuplicateArgument { - location, + locations: vec![location, other_location], label: label.to_string(), }); } diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index 87b04aef..64b43fca 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -240,12 +240,10 @@ impl<'a, 'b> PatternTyper<'a, 'b> { .environment .get_variable(&name) .cloned() - .ok_or_else(|| { - Error::unknown_variable_or_type( - location, - &name, - self.environment.local_value_names(), - ) + .ok_or_else(|| Error::UnknownVariable { + location, + name: name.to_string(), + variables: self.environment.local_value_names(), })?; self.environment.increment_usage(&name); @@ -359,8 +357,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { Pattern::Tuple { elems, location } => match collapse_links(tipo.clone()).deref() { Type::Tuple { elems: type_elems } => { if elems.len() != type_elems.len() { - return Err(Error::IncorrectArity { - labels: vec![], + return Err(Error::IncorrectTupleArity { location, expected: type_elems.len(), given: elems.len(), @@ -487,14 +484,14 @@ impl<'a, 'b> PatternTyper<'a, 'b> { // The fun has no field map and so we error if arguments have been labelled None => assert_no_labeled_arguments(&pattern_args) .map(|(location, label)| { - Err(Error::unexpected_labeled_arg_in_pattern( + Err(Error::UnexpectedLabeledArgInPattern { location, label, - &name, - &pattern_args, - &module, + name: name.clone(), + args: pattern_args.clone(), + module: module.clone(), with_spread, - )) + }) }) .unwrap_or(Ok(()))?, } @@ -555,11 +552,13 @@ impl<'a, 'b> PatternTyper<'a, 'b> { is_record, }) } else { - Err(Error::IncorrectArity { - labels: vec![], + Err(Error::IncorrectPatternArity { location, - expected: args.len(), - given: pattern_args.len(), + given: pattern_args, + expected: 0, + name: name.clone(), + module: module.clone(), + is_record, }) } } @@ -583,11 +582,13 @@ impl<'a, 'b> PatternTyper<'a, 'b> { is_record, }) } else { - Err(Error::IncorrectArity { - labels: vec![], + Err(Error::IncorrectPatternArity { location, + given: pattern_args, expected: 0, - given: pattern_args.len(), + name: name.clone(), + module: module.clone(), + is_record, }) } } diff --git a/crates/aiken-lang/src/uplc.rs b/crates/aiken-lang/src/uplc.rs index 571bc911..7b436c42 100644 --- a/crates/aiken-lang/src/uplc.rs +++ b/crates/aiken-lang/src/uplc.rs @@ -972,7 +972,8 @@ impl<'a> CodeGenerator<'a> { .iter() .map(|item| { let label = item.label.clone().unwrap_or_default(); - let field_index = field_map.fields.get(&label).unwrap_or(&0); + let field_index = + field_map.fields.get(&label).map(|x| &x.0).unwrap_or(&0); let (discard, var_name) = match &item.value { Pattern::Var { name, .. } => (false, name.clone()), Pattern::Discard { .. } => (true, "".to_string()), @@ -1316,7 +1317,8 @@ impl<'a> CodeGenerator<'a> { .iter() .map(|item| { let label = item.label.clone().unwrap_or_default(); - let field_index = field_map.fields.get(&label).unwrap_or(&0); + let field_index = + field_map.fields.get(&label).map(|x| &x.0).unwrap_or(&0); let (discard, var_name) = match &item.value { Pattern::Var { name, .. } => (false, name.clone()), Pattern::Discard { .. } => (true, "".to_string()), @@ -2031,7 +2033,11 @@ impl<'a> CodeGenerator<'a> { for field in field_map .fields .iter() - .sorted_by(|item1, item2| item1.1.cmp(item2.1)) + .sorted_by(|item1, item2| { + let (a, _) = item1.1; + let (b, _) = item2.1; + a.cmp(b) + }) .zip(&args_type) .rev() { @@ -2092,7 +2098,11 @@ impl<'a> CodeGenerator<'a> { for field in field_map .fields .iter() - .sorted_by(|item1, item2| item1.1.cmp(item2.1)) + .sorted_by(|item1, item2| { + let (a, _) = item1.1; + let (b, _) = item2.1; + a.cmp(b) + }) .rev() { term = Term::Lambda { diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index 38455c2e..3f65a5b5 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -18,17 +18,17 @@ use zip_extract::ZipExtractError; #[allow(dead_code)] #[derive(thiserror::Error)] pub enum Error { - #[error("Duplicate module\n\n{module}")] + #[error("I just found two modules with the same name: '{module}'")] DuplicateModule { module: String, first: PathBuf, second: PathBuf, }, - #[error("File operation failed")] + #[error("Some operation on the file-system did fail.")] FileIo { error: io::Error, path: PathBuf }, - #[error("Source code incorrectly formatted")] + #[error("I found some files with incorrectly formatted source code.")] Format { problem_files: Vec }, #[error(transparent)] @@ -52,29 +52,26 @@ pub enum Error { help: String, }, - #[error("Missing 'aiken.toml' manifest in {path}")] + #[error("I couldn't find any 'aiken.toml' manifest in {path}.")] MissingManifest { path: PathBuf }, - #[error("Cyclical module imports")] + #[error("I just found a cycle in module hierarchy!")] ImportCycle { modules: Vec }, /// Useful for returning many [`Error::Parse`] at once #[error("A list of errors")] List(Vec), - #[error("Parsing")] + #[error("While parsing files...")] Parse { path: PathBuf, - src: String, - named: NamedSource, - #[source] error: Box, }, - #[error("Type-checking")] + #[error("While trying to make sense of your code...")] Type { path: PathBuf, src: String, @@ -246,7 +243,10 @@ impl Diagnostic for Error { Error::ImportCycle { .. } => Some(Box::new("aiken::module::cyclical")), Error::List(_) => None, Error::Parse { .. } => Some(Box::new("aiken::parser")), - Error::Type { .. } => Some(Box::new("aiken::check")), + Error::Type { error, .. } => Some(Box::new(format!( + "err::aiken::check{}", + error.code().map(|s| format!("::{s}")).unwrap_or_default() + ))), Error::StandardIo(_) => None, Error::MissingManifest { .. } => None, Error::TomlLoading { .. } => Some(Box::new("aiken::loading::toml")),