diff --git a/cli/src/main.rs b/cli/src/main.rs index 596dc41768..9ae76c2a5e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -156,8 +156,7 @@ fn gen( target: Triple, dest_filename: &Path, ) { - use roc_reporting::report::{can_problem, RocDocAllocator, DEFAULT_PALETTE}; - use roc_reporting::type_error::type_problem; + use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE}; let src = loaded.src; let home = loaded.module_id; diff --git a/compiler/reporting/src/error/canonicalize.rs b/compiler/reporting/src/error/canonicalize.rs new file mode 100644 index 0000000000..9d691a7ffb --- /dev/null +++ b/compiler/reporting/src/error/canonicalize.rs @@ -0,0 +1,397 @@ +use roc_collections::all::MutSet; +use roc_problem::can::PrecedenceProblem::BothNonAssociative; +use roc_problem::can::{Problem, RuntimeError}; +use std::path::PathBuf; + +use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder}; +use ven_pretty::DocAllocator; + +pub fn can_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: PathBuf, + problem: Problem, +) -> Report<'b> { + let doc = match problem { + Problem::UnusedDef(symbol, region) => { + let line = + r#" then remove it so future readers of your code don't wonder why it is there."#; + + alloc.stack(vec![ + alloc + .symbol_unqualified(symbol) + .append(alloc.reflow(" is not used anywhere in your code.")), + alloc.region(region), + alloc + .reflow("If you didn't intend on using ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(line)), + ]) + } + Problem::UnusedImport(module_id, region) => alloc.concat(vec![ + alloc.reflow("Nothing from "), + alloc.module(module_id), + alloc.reflow(" is used in this module."), + alloc.region(region), + alloc.reflow("Since "), + alloc.module(module_id), + alloc.reflow(" isn't used, you don't need to import it."), + ]), + Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { + let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; + + alloc.concat(vec![ + alloc.symbol_unqualified(closure_symbol), + alloc.reflow(" doesn't use "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow("."), + alloc.region(region), + alloc.reflow("If you don't need "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(", then you can just remove it. However, if you really do need "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(" as an argument of "), + alloc.symbol_unqualified(closure_symbol), + alloc.reflow(", prefix it with an underscore, like this: \"_"), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(line), + ]) + } + Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => alloc + .stack(vec![ + if left_bin_op.value == right_bin_op.value { + alloc.concat(vec![ + alloc.reflow("Using more than one "), + alloc.binop(left_bin_op.value), + alloc.reflow(concat!( + " like this requires parentheses,", + " to clarify how things should be grouped.", + )), + ]) + } else { + alloc.concat(vec![ + alloc.reflow("Using "), + alloc.binop(left_bin_op.value), + alloc.reflow(" and "), + alloc.binop(right_bin_op.value), + alloc.reflow(concat!( + " together requires parentheses, ", + "to clarify how they should be grouped." + )), + ]) + }, + alloc.region(region), + ]), + Problem::UnsupportedPattern(pattern_type, region) => { + use roc_parse::pattern::PatternType::*; + + let this_thing = match pattern_type { + TopLevelDef => "a top-level definition:", + DefExpr => "a value definition:", + FunctionArg => "function arguments:", + WhenBranch => unreachable!("all patterns are allowed in a When"), + }; + + let suggestion = vec![ + alloc.reflow( + "Patterns like this don't cover all possible shapes of the input type. Use a ", + ), + alloc.keyword("when"), + alloc.reflow(" ... "), + alloc.keyword("is"), + alloc.reflow(" instead."), + ]; + + alloc.stack(vec![ + alloc + .reflow("This pattern is not allowed in ") + .append(alloc.reflow(this_thing)), + alloc.region(region), + alloc.concat(suggestion), + ]) + } + Problem::ShadowingInAnnotation { + original_region, + shadow, + } => pretty_runtime_error( + alloc, + RuntimeError::Shadowing { + original_region, + shadow, + }, + ), + Problem::CyclicAlias(symbol, region, others) => { + let (doc, title) = crate::error::r#type::cyclic_alias(alloc, symbol, region, others); + + return Report { + filename, + title, + doc, + }; + } + Problem::PhantomTypeArgument { + alias, + variable_region, + variable_name, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("The "), + alloc.type_variable(variable_name), + alloc.reflow(" type variable is not used in the "), + alloc.symbol_unqualified(alias), + alloc.reflow(" alias definition:"), + ]), + alloc.region(variable_region), + alloc.reflow("Roc does not allow unused type parameters!"), + // TODO add link to this guide section + alloc.hint().append(alloc.reflow( + "If you want an unused type parameter (a so-called \"phantom type\"), \ + read the guide section on phantom data.", + )), + ]), + Problem::DuplicateRecordFieldValue { + field_name, + field_region, + record_region, + replaced_region, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This record defines the "), + alloc.record_field(field_name.clone()), + alloc.reflow(" field twice!"), + ]), + alloc.region_all_the_things( + record_region, + replaced_region, + field_region, + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + record_region, + field_region, + field_region, + Annotation::TypoSuggestion, + ), + alloc.concat(vec![ + alloc.reflow("For clarity, remove the previous "), + alloc.record_field(field_name), + alloc.reflow(" definitions from this record."), + ]), + ]), + Problem::DuplicateRecordFieldType { + field_name, + field_region, + record_region, + replaced_region, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This record type defines the "), + alloc.record_field(field_name.clone()), + alloc.reflow(" field twice!"), + ]), + alloc.region_all_the_things( + record_region, + replaced_region, + field_region, + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + record_region, + field_region, + field_region, + Annotation::TypoSuggestion, + ), + alloc.concat(vec![ + alloc.reflow("For clarity, remove the previous "), + alloc.record_field(field_name), + alloc.reflow(" definitions from this record type."), + ]), + ]), + Problem::DuplicateTag { + tag_name, + tag_union_region, + tag_region, + replaced_region, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This tag union type defines the "), + alloc.tag_name(tag_name.clone()), + alloc.reflow(" tag twice!"), + ]), + alloc.region_all_the_things( + tag_union_region, + replaced_region, + tag_region, + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + tag_union_region, + tag_region, + tag_region, + Annotation::TypoSuggestion, + ), + alloc.concat(vec![ + alloc.reflow("For clarity, remove the previous "), + alloc.tag_name(tag_name), + alloc.reflow(" definitions from this tag union type."), + ]), + ]), + Problem::RuntimeError(runtime_error) => pretty_runtime_error(alloc, runtime_error), + }; + + Report { + title: "SYNTAX PROBLEM".to_string(), + filename, + doc, + } +} + +fn pretty_runtime_error<'b>( + alloc: &'b RocDocAllocator<'b>, + runtime_error: RuntimeError, +) -> RocDocBuilder<'b> { + match runtime_error { + RuntimeError::Shadowing { + original_region, + shadow, + } => { + let line = r#"Since these variables have the same name, it's easy to use the wrong one on accident. Give one of them a new name."#; + + alloc.stack(vec![ + alloc + .text("The ") + .append(alloc.ident(shadow.value)) + .append(alloc.reflow(" name is first defined here:")), + alloc.region(original_region), + alloc.reflow("But then it's defined a second time here:"), + alloc.region(shadow.region), + alloc.reflow(line), + ]) + } + + RuntimeError::LookupNotInScope(loc_name, options) => { + not_found(alloc, loc_name.region, &loc_name.value, "value", options) + } + RuntimeError::CircularDef(mut idents, regions) => { + let first = idents.remove(0); + + if idents.is_empty() { + alloc + .reflow("The ") + .append(alloc.ident(first.value.clone())) + .append(alloc.reflow( + " value is defined directly in terms of itself, causing an infinite loop.", + )) + // TODO "are you trying to mutate a variable? + // TODO hint? + } else { + alloc.stack(vec![ + alloc + .reflow("The ") + .append(alloc.ident(first.value.clone())) + .append( + alloc.reflow(" definition is causing a very tricky infinite loop:"), + ), + alloc.region(regions[0].0), + alloc + .reflow("The ") + .append(alloc.ident(first.value.clone())) + .append(alloc.reflow( + " value depends on itself through the following chain of definitions:", + )), + crate::report::cycle( + alloc, + 4, + alloc.ident(first.value), + idents + .into_iter() + .map(|ident| alloc.ident(ident.value)) + .collect::>(), + ), + // TODO hint? + ]) + } + } + other => { + // // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! + // UnsupportedPattern(Region), + // UnrecognizedFunctionName(Located), + // SymbolNotExposed { + // module_name: InlinableString, + // ident: InlinableString, + // region: Region, + // }, + // ModuleNotImported { + // module_name: InlinableString, + // ident: InlinableString, + // region: Region, + // }, + // InvalidPrecedence(PrecedenceProblem, Region), + // MalformedIdentifier(Box, Region), + // MalformedClosure(Region), + // FloatOutsideRange(Box), + // IntOutsideRange(Box), + // InvalidHex(std::num::ParseIntError, Box), + // InvalidOctal(std::num::ParseIntError, Box), + // InvalidBinary(std::num::ParseIntError, Box), + // QualifiedPatternIdent(InlinableString), + // CircularDef( + // Vec>, + // Vec<(Region /* pattern */, Region /* expr */)>, + // ), + // + // /// When the author specifies a type annotation but no implementation + // NoImplementation, + todo!("TODO implement run time error reporting for {:?}", other) + } + } +} + +fn not_found<'b>( + alloc: &'b RocDocAllocator<'b>, + region: roc_region::all::Region, + name: &str, + thing: &'b str, + options: MutSet>, +) -> RocDocBuilder<'b> { + use crate::error::r#type::suggest; + + let mut suggestions = suggest::sort(name, options.iter().map(|v| v.as_ref()).collect()); + suggestions.truncate(4); + + let default_no = alloc.concat(vec![ + alloc.reflow("Is there an "), + alloc.keyword("import"), + alloc.reflow(" or "), + alloc.keyword("exposing"), + alloc.reflow(" missing up-top"), + ]); + + let default_yes = alloc.reflow("these names seem close though:"); + + let to_details = |no_suggestion_details, yes_suggestion_details| { + if suggestions.is_empty() { + no_suggestion_details + } else { + alloc.stack(vec![ + yes_suggestion_details, + alloc + .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) + .indent(4), + ]) + } + }; + + alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("I cannot find a `"), + alloc.string(name.to_string()), + alloc.reflow("` "), + alloc.reflow(thing), + ]), + alloc.region(region), + to_details(default_no, default_yes), + ]) +} diff --git a/compiler/reporting/src/error/mod.rs b/compiler/reporting/src/error/mod.rs new file mode 100644 index 0000000000..2bf7e77288 --- /dev/null +++ b/compiler/reporting/src/error/mod.rs @@ -0,0 +1,4 @@ +pub mod canonicalize; +pub mod mono; +pub mod parse; +pub mod r#type; diff --git a/compiler/reporting/src/error/mono.rs b/compiler/reporting/src/error/mono.rs new file mode 100644 index 0000000000..12f9f54801 --- /dev/null +++ b/compiler/reporting/src/error/mono.rs @@ -0,0 +1,147 @@ +use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder}; +use std::path::PathBuf; +use ven_pretty::DocAllocator; + +pub fn mono_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: PathBuf, + problem: roc_mono::expr::MonoProblem, +) -> Report<'b> { + use roc_mono::expr::MonoProblem::*; + use roc_mono::pattern::Context::*; + use roc_mono::pattern::Error::*; + + match problem { + PatternProblem(Incomplete(region, context, missing)) => match context { + BadArg => { + let doc = alloc.stack(vec![ + alloc.reflow("This pattern does not cover all the possibilities:"), + alloc.region(region), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.concat(vec![ + alloc.reflow( + "I would have to crash if I saw one of those! \ + So rather than pattern matching in function arguments, put a ", + ), + alloc.keyword("when"), + alloc.reflow(" in the function body to account for all possibilities."), + ]), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + } + } + BadDestruct => { + let doc = alloc.stack(vec![ + alloc.reflow("This pattern does not cover all the possibilities:"), + alloc.region(region), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.concat(vec![ + alloc.reflow( + "I would have to crash if I saw one of those! \ + You can use a binding to deconstruct a value if there is only ONE possibility. \ + Use a " + ), + alloc.keyword("when"), + alloc.reflow(" to account for all possibilities."), + ]), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + } + } + BadCase => { + let doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This "), + alloc.keyword("when"), + alloc.reflow(" does not cover all the possibilities:"), + ]), + alloc.region(region), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.reflow( + "I would have to crash if I saw one of those! \ + Add branches for them!", + ), + // alloc.hint().append(alloc.reflow("or use a hole.")), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + } + } + }, + PatternProblem(Redundant { + overall_region, + branch_region, + index, + }) => { + let doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" pattern is redundant:"), + ]), + alloc.region_with_subregion(overall_region, branch_region), + alloc.reflow( + "Any value of this shape will be handled by \ + a previous pattern, so this one should be removed.", + ), + ]); + + Report { + filename, + title: "REDUNDANT PATTERN".to_string(), + doc, + } + } + } +} + +pub fn unhandled_patterns_to_doc_block<'b>( + alloc: &'b RocDocAllocator<'b>, + patterns: Vec, +) -> RocDocBuilder<'b> { + alloc + .vcat(patterns.into_iter().map(|v| pattern_to_doc(alloc, v))) + .indent(4) + .annotate(Annotation::TypeBlock) +} + +fn pattern_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + pattern: roc_mono::pattern::Pattern, +) -> RocDocBuilder<'b> { + use roc_mono::pattern::Literal::*; + use roc_mono::pattern::Pattern::*; + + match pattern { + Anything => alloc.text("_"), + Literal(l) => match l { + Int(i) => alloc.text(i.to_string()), + Bit(true) => alloc.text("True"), + Bit(false) => alloc.text("False"), + Byte(b) => alloc.text(b.to_string()), + Float(f) => alloc.text(f.to_string()), + Str(s) => alloc.string(s.into()), + }, + Ctor(_, tag_name, args) => { + let arg_docs = args.into_iter().map(|v| pattern_to_doc(alloc, v)); + + let docs = std::iter::once(alloc.tag_name(tag_name)).chain(arg_docs); + + alloc.intersperse(docs, alloc.space()) + } + } +} diff --git a/compiler/reporting/src/error/parse.rs b/compiler/reporting/src/error/parse.rs new file mode 100644 index 0000000000..450f7f4759 --- /dev/null +++ b/compiler/reporting/src/error/parse.rs @@ -0,0 +1,39 @@ +use roc_parse::parser::{Fail, FailReason}; +use std::path::PathBuf; + +use crate::report::{Report, RocDocAllocator}; +use ven_pretty::DocAllocator; + +pub fn parse_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: PathBuf, + problem: Fail, +) -> Report<'b> { + use FailReason::*; + + match problem.reason { + ArgumentsBeforeEquals => { + let doc = alloc.text("Unexpected tokens in front of the `=` symbol:"); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + } + } + other => { + // + // Unexpected(char, Region), + // OutdentedTooFar, + // ConditionFailed, + // LineTooLong(u32 /* which line was too long */), + // TooManyLines, + // Eof(Region), + // InvalidPattern, + // ReservedKeyword(Region), + // ArgumentsBeforeEquals, + //} + todo!("unhandled parse error: {:?}", other) + } + } +} diff --git a/compiler/reporting/src/type_error.rs b/compiler/reporting/src/error/type.rs similarity index 99% rename from compiler/reporting/src/type_error.rs rename to compiler/reporting/src/error/type.rs index a6da44ac31..38c29d6c4e 100644 --- a/compiler/reporting/src/type_error.rs +++ b/compiler/reporting/src/error/type.rs @@ -1951,7 +1951,7 @@ mod report_text { fs: Vec<(Lowercase, ErrorType)>, ext: TypeExt, ) -> RocDocBuilder<'b> { - use crate::type_error::{ext_to_doc, to_doc}; + use crate::error::r#type::{ext_to_doc, to_doc}; let entry_to_doc = |(name, tipe): (Lowercase, ErrorType)| { ( @@ -2129,9 +2129,9 @@ mod report_text { fn type_problem_to_pretty<'b>( alloc: &'b RocDocAllocator<'b>, - problem: crate::type_error::Problem, + problem: crate::error::r#type::Problem, ) -> RocDocBuilder<'b> { - use crate::type_error::Problem::*; + use crate::error::r#type::Problem::*; match problem { FieldTypo(typo, possibilities) => { diff --git a/compiler/reporting/src/lib.rs b/compiler/reporting/src/lib.rs index 36507c33e1..12ef1d2a39 100644 --- a/compiler/reporting/src/lib.rs +++ b/compiler/reporting/src/lib.rs @@ -11,5 +11,5 @@ // re-enable this when working on performance optimizations than have it block PRs. #![allow(clippy::large_enum_variant)] +pub mod error; pub mod report; -pub mod type_error; diff --git a/compiler/reporting/src/report.rs b/compiler/reporting/src/report.rs index 2df5637ceb..25cb34c2a9 100644 --- a/compiler/reporting/src/report.rs +++ b/compiler/reporting/src/report.rs @@ -1,13 +1,15 @@ -use roc_collections::all::MutSet; use roc_module::ident::Ident; use roc_module::ident::{Lowercase, TagName, Uppercase}; use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_problem::can::PrecedenceProblem::BothNonAssociative; -use roc_problem::can::{Problem, RuntimeError}; use std::fmt; use std::path::PathBuf; use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated}; +pub use crate::error::canonicalize::can_problem; +pub use crate::error::mono::mono_problem; +pub use crate::error::parse::parse_problem; +pub use crate::error::r#type::type_problem; + // const IS_WINDOWS: bool = std::env::consts::OS == "windows"; const IS_WINDOWS: bool = false; @@ -144,541 +146,6 @@ pub const UNDERLINE_CODE: &str = "\u{001b}[4m"; pub const RESET_CODE: &str = "\u{001b}[0m"; -pub fn mono_problem<'b>( - alloc: &'b RocDocAllocator<'b>, - filename: PathBuf, - problem: roc_mono::expr::MonoProblem, -) -> Report<'b> { - use roc_mono::expr::MonoProblem::*; - use roc_mono::pattern::Context::*; - use roc_mono::pattern::Error::*; - - match problem { - PatternProblem(Incomplete(region, context, missing)) => match context { - BadArg => { - let doc = alloc.stack(vec![ - alloc.reflow("This pattern does not cover all the possibilities:"), - alloc.region(region), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.concat(vec![ - alloc.reflow( - "I would have to crash if I saw one of those! \ - So rather than pattern matching in function arguments, put a ", - ), - alloc.keyword("when"), - alloc.reflow(" in the function body to account for all possibilities."), - ]), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - } - } - BadDestruct => { - let doc = alloc.stack(vec![ - alloc.reflow("This pattern does not cover all the possibilities:"), - alloc.region(region), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.concat(vec![ - alloc.reflow( - "I would have to crash if I saw one of those! \ - You can use a binding to deconstruct a value if there is only ONE possibility. \ - Use a " - ), - alloc.keyword("when"), - alloc.reflow(" to account for all possibilities."), - ]), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - } - } - BadCase => { - let doc = alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("This "), - alloc.keyword("when"), - alloc.reflow(" does not cover all the possibilities:"), - ]), - alloc.region(region), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.reflow( - "I would have to crash if I saw one of those! \ - Add branches for them!", - ), - // alloc.hint().append(alloc.reflow("or use a hole.")), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - } - } - }, - PatternProblem(Redundant { - overall_region, - branch_region, - index, - }) => { - let doc = alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("The "), - alloc.string(index.ordinal()), - alloc.reflow(" pattern is redundant:"), - ]), - alloc.region_with_subregion(overall_region, branch_region), - alloc.reflow( - "Any value of this shape will be handled by \ - a previous pattern, so this one should be removed.", - ), - ]); - - Report { - filename, - title: "REDUNDANT PATTERN".to_string(), - doc, - } - } - } -} - -pub fn unhandled_patterns_to_doc_block<'b>( - alloc: &'b RocDocAllocator<'b>, - patterns: Vec, -) -> RocDocBuilder<'b> { - alloc - .vcat(patterns.into_iter().map(|v| pattern_to_doc(alloc, v))) - .indent(4) - .annotate(Annotation::TypeBlock) -} - -fn pattern_to_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - pattern: roc_mono::pattern::Pattern, -) -> RocDocBuilder<'b> { - use roc_mono::pattern::Literal::*; - use roc_mono::pattern::Pattern::*; - // Anything, - // Literal(Literal), - // Ctor(Union, TagName, std::vec::Vec), - match pattern { - Anything => alloc.text("_"), - Literal(l) => match l { - Int(i) => alloc.text(i.to_string()), - Bit(true) => alloc.text("True"), - Bit(false) => alloc.text("False"), - Byte(b) => alloc.text(b.to_string()), - Float(f) => alloc.text(f.to_string()), - Str(s) => alloc.string(s.into()), - }, - Ctor(_, tag_name, args) => { - let arg_docs = args.into_iter().map(|v| pattern_to_doc(alloc, v)); - - let docs = std::iter::once(alloc.tag_name(tag_name)).chain(arg_docs); - - alloc.intersperse(docs, alloc.space()) - } - } -} - -pub fn can_problem<'b>( - alloc: &'b RocDocAllocator<'b>, - filename: PathBuf, - problem: Problem, -) -> Report<'b> { - let doc = match problem { - Problem::UnusedDef(symbol, region) => { - let line = - r#" then remove it so future readers of your code don't wonder why it is there."#; - - alloc.stack(vec![ - alloc - .symbol_unqualified(symbol) - .append(alloc.reflow(" is not used anywhere in your code.")), - alloc.region(region), - alloc - .reflow("If you didn't intend on using ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow(line)), - ]) - } - Problem::UnusedImport(module_id, region) => alloc.concat(vec![ - alloc.reflow("Nothing from "), - alloc.module(module_id), - alloc.reflow(" is used in this module."), - alloc.region(region), - alloc.reflow("Since "), - alloc.module(module_id), - alloc.reflow(" isn't used, you don't need to import it."), - ]), - Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { - let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; - - alloc.concat(vec![ - alloc.symbol_unqualified(closure_symbol), - alloc.reflow(" doesn't use "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow("."), - alloc.region(region), - alloc.reflow("If you don't need "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(", then you can just remove it. However, if you really do need "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(" as an argument of "), - alloc.symbol_unqualified(closure_symbol), - alloc.reflow(", prefix it with an underscore, like this: \"_"), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(line), - ]) - } - Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => alloc - .stack(vec![ - if left_bin_op.value == right_bin_op.value { - alloc.concat(vec![ - alloc.reflow("Using more than one "), - alloc.binop(left_bin_op.value), - alloc.reflow(concat!( - " like this requires parentheses,", - " to clarify how things should be grouped.", - )), - ]) - } else { - alloc.concat(vec![ - alloc.reflow("Using "), - alloc.binop(left_bin_op.value), - alloc.reflow(" and "), - alloc.binop(right_bin_op.value), - alloc.reflow(concat!( - " together requires parentheses, ", - "to clarify how they should be grouped." - )), - ]) - }, - alloc.region(region), - ]), - Problem::UnsupportedPattern(pattern_type, region) => { - use roc_parse::pattern::PatternType::*; - - let this_thing = match pattern_type { - TopLevelDef => "a top-level definition:", - DefExpr => "a value definition:", - FunctionArg => "function arguments:", - WhenBranch => unreachable!("all patterns are allowed in a When"), - }; - - let suggestion = vec![ - alloc.reflow( - "Patterns like this don't cover all possible shapes of the input type. Use a ", - ), - alloc.keyword("when"), - alloc.reflow(" ... "), - alloc.keyword("is"), - alloc.reflow(" instead."), - ]; - - alloc.stack(vec![ - alloc - .reflow("This pattern is not allowed in ") - .append(alloc.reflow(this_thing)), - alloc.region(region), - alloc.concat(suggestion), - ]) - } - Problem::ShadowingInAnnotation { - original_region, - shadow, - } => pretty_runtime_error( - alloc, - RuntimeError::Shadowing { - original_region, - shadow, - }, - ), - Problem::CyclicAlias(symbol, region, others) => { - let (doc, title) = crate::type_error::cyclic_alias(alloc, symbol, region, others); - - return Report { - filename, - title, - doc, - }; - } - Problem::PhantomTypeArgument { - alias, - variable_region, - variable_name, - } => alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("The "), - alloc.type_variable(variable_name), - alloc.reflow(" type variable is not used in the "), - alloc.symbol_unqualified(alias), - alloc.reflow(" alias definition:"), - ]), - alloc.region(variable_region), - alloc.reflow("Roc does not allow unused type parameters!"), - // TODO add link to this guide section - alloc.hint().append(alloc.reflow( - "If you want an unused type parameter (a so-called \"phantom type\"), \ - read the guide section on phantom data.", - )), - ]), - Problem::DuplicateRecordFieldValue { - field_name, - field_region, - record_region, - replaced_region, - } => alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("This record defines the "), - alloc.record_field(field_name.clone()), - alloc.reflow(" field twice!"), - ]), - alloc.region_all_the_things( - record_region, - replaced_region, - field_region, - Annotation::Error, - ), - alloc.reflow("In the rest of the program, I will only use the latter definition:"), - alloc.region_all_the_things( - record_region, - field_region, - field_region, - Annotation::TypoSuggestion, - ), - alloc.concat(vec![ - alloc.reflow("For clarity, remove the previous "), - alloc.record_field(field_name), - alloc.reflow(" definitions from this record."), - ]), - ]), - Problem::DuplicateRecordFieldType { - field_name, - field_region, - record_region, - replaced_region, - } => alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("This record type defines the "), - alloc.record_field(field_name.clone()), - alloc.reflow(" field twice!"), - ]), - alloc.region_all_the_things( - record_region, - replaced_region, - field_region, - Annotation::Error, - ), - alloc.reflow("In the rest of the program, I will only use the latter definition:"), - alloc.region_all_the_things( - record_region, - field_region, - field_region, - Annotation::TypoSuggestion, - ), - alloc.concat(vec![ - alloc.reflow("For clarity, remove the previous "), - alloc.record_field(field_name), - alloc.reflow(" definitions from this record type."), - ]), - ]), - Problem::DuplicateTag { - tag_name, - tag_union_region, - tag_region, - replaced_region, - } => alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("This tag union type defines the "), - alloc.tag_name(tag_name.clone()), - alloc.reflow(" tag twice!"), - ]), - alloc.region_all_the_things( - tag_union_region, - replaced_region, - tag_region, - Annotation::Error, - ), - alloc.reflow("In the rest of the program, I will only use the latter definition:"), - alloc.region_all_the_things( - tag_union_region, - tag_region, - tag_region, - Annotation::TypoSuggestion, - ), - alloc.concat(vec![ - alloc.reflow("For clarity, remove the previous "), - alloc.tag_name(tag_name), - alloc.reflow(" definitions from this tag union type."), - ]), - ]), - Problem::RuntimeError(runtime_error) => pretty_runtime_error(alloc, runtime_error), - }; - - Report { - title: "SYNTAX PROBLEM".to_string(), - filename, - doc, - } -} - -fn not_found<'b>( - alloc: &'b RocDocAllocator<'b>, - region: roc_region::all::Region, - name: &str, - thing: &'b str, - options: MutSet>, -) -> RocDocBuilder<'b> { - use crate::type_error::suggest; - - let mut suggestions = suggest::sort(name, options.iter().map(|v| v.as_ref()).collect()); - suggestions.truncate(4); - - let default_no = alloc.concat(vec![ - alloc.reflow("Is there an "), - alloc.keyword("import"), - alloc.reflow(" or "), - alloc.keyword("exposing"), - alloc.reflow(" missing up-top"), - ]); - - let default_yes = alloc.reflow("these names seem close though:"); - - let to_details = |no_suggestion_details, yes_suggestion_details| { - if suggestions.is_empty() { - no_suggestion_details - } else { - alloc.stack(vec![ - yes_suggestion_details, - alloc - .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) - .indent(4), - ]) - } - }; - - alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("I cannot find a `"), - alloc.string(name.to_string()), - alloc.reflow("` "), - alloc.reflow(thing), - ]), - alloc.region(region), - to_details(default_no, default_yes), - ]) -} -fn pretty_runtime_error<'b>( - alloc: &'b RocDocAllocator<'b>, - runtime_error: RuntimeError, -) -> RocDocBuilder<'b> { - match runtime_error { - RuntimeError::Shadowing { - original_region, - shadow, - } => { - let line = r#"Since these variables have the same name, it's easy to use the wrong one on accident. Give one of them a new name."#; - - alloc.stack(vec![ - alloc - .text("The ") - .append(alloc.ident(shadow.value)) - .append(alloc.reflow(" name is first defined here:")), - alloc.region(original_region), - alloc.reflow("But then it's defined a second time here:"), - alloc.region(shadow.region), - alloc.reflow(line), - ]) - } - - RuntimeError::LookupNotInScope(loc_name, options) => { - not_found(alloc, loc_name.region, &loc_name.value, "value", options) - } - RuntimeError::CircularDef(mut idents, regions) => { - let first = idents.remove(0); - - if idents.is_empty() { - alloc - .reflow("The ") - .append(alloc.ident(first.value.clone())) - .append(alloc.reflow( - " value is defined directly in terms of itself, causing an infinite loop.", - )) - // TODO "are you trying to mutate a variable? - // TODO hint? - } else { - alloc.stack(vec![ - alloc - .reflow("The ") - .append(alloc.ident(first.value.clone())) - .append( - alloc.reflow(" definition is causing a very tricky infinite loop:"), - ), - alloc.region(regions[0].0), - alloc - .reflow("The ") - .append(alloc.ident(first.value.clone())) - .append(alloc.reflow( - " value depends on itself through the following chain of definitions:", - )), - cycle( - alloc, - 4, - alloc.ident(first.value), - idents - .into_iter() - .map(|ident| alloc.ident(ident.value)) - .collect::>(), - ), - // TODO hint? - ]) - } - } - other => { - // // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! - // UnsupportedPattern(Region), - // UnrecognizedFunctionName(Located), - // SymbolNotExposed { - // module_name: InlinableString, - // ident: InlinableString, - // region: Region, - // }, - // ModuleNotImported { - // module_name: InlinableString, - // ident: InlinableString, - // region: Region, - // }, - // InvalidPrecedence(PrecedenceProblem, Region), - // MalformedIdentifier(Box, Region), - // MalformedClosure(Region), - // FloatOutsideRange(Box), - // IntOutsideRange(Box), - // InvalidHex(std::num::ParseIntError, Box), - // InvalidOctal(std::num::ParseIntError, Box), - // InvalidBinary(std::num::ParseIntError, Box), - // QualifiedPatternIdent(InlinableString), - // CircularDef( - // Vec>, - // Vec<(Region /* pattern */, Region /* expr */)>, - // ), - // - // /// When the author specifies a type annotation but no implementation - // NoImplementation, - todo!("TODO implement run time error reporting for {:?}", other) - } - } -} - // define custom allocator struct so we can `impl RocDocAllocator` custom helpers pub struct RocDocAllocator<'a> { upstream: BoxAllocator, diff --git a/compiler/reporting/tests/helpers/mod.rs b/compiler/reporting/tests/helpers/mod.rs index 040d21e97d..67c3bbc46b 100644 --- a/compiler/reporting/tests/helpers/mod.rs +++ b/compiler/reporting/tests/helpers/mod.rs @@ -103,7 +103,7 @@ pub fn parse_loc_with<'a>(arena: &'a Bump, input: &'a str) -> Result Result { +pub fn can_expr(expr_str: &str) -> Result { can_expr_with(&Bump::new(), test_home(), expr_str) } @@ -118,12 +118,32 @@ pub struct CanExprOut { pub constraint: Constraint, } +#[derive(Debug)] +pub struct ParseErrOut { + pub fail: Fail, + pub home: ModuleId, + pub interns: Interns, +} + #[allow(dead_code)] -pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> Result { +pub fn can_expr_with( + arena: &Bump, + home: ModuleId, + expr_str: &str, +) -> Result { let loc_expr = match parse_loc_with(&arena, expr_str) { Ok(e) => e, Err(fail) => { - return Err(fail); + let interns = Interns { + module_ids: ModuleIds::default(), + all_ident_ids: MutMap::default(), + }; + + return Err(ParseErrOut { + fail, + interns, + home, + }); } }; diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index 39aebc8140..8c4b3ad9d7 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -14,16 +14,15 @@ mod test_reporting { use roc_module::symbol::{Interns, ModuleId}; use roc_mono::expr::{Expr, Procs}; use roc_reporting::report::{ - can_problem, mono_problem, Report, BLUE_CODE, BOLD_CODE, CYAN_CODE, DEFAULT_PALETTE, - GREEN_CODE, MAGENTA_CODE, RED_CODE, RESET_CODE, UNDERLINE_CODE, WHITE_CODE, YELLOW_CODE, + can_problem, mono_problem, parse_problem, type_problem, Report, BLUE_CODE, BOLD_CODE, + CYAN_CODE, DEFAULT_PALETTE, GREEN_CODE, MAGENTA_CODE, RED_CODE, RESET_CODE, UNDERLINE_CODE, + WHITE_CODE, YELLOW_CODE, }; - use roc_reporting::type_error::type_problem; use roc_types::pretty_print::name_all_type_vars; use roc_types::subs::Subs; use std::path::PathBuf; // use roc_region::all; - use crate::helpers::{can_expr, infer_expr, CanExprOut}; - use roc_parse::parser::Fail; + use crate::helpers::{can_expr, infer_expr, CanExprOut, ParseErrOut}; use roc_reporting::report::{RocDocAllocator, RocDocBuilder}; use roc_solve::solve; @@ -52,7 +51,7 @@ mod test_reporting { ModuleId, Interns, ), - Fail, + ParseErrOut, > { let CanExprOut { loc_expr, @@ -112,15 +111,29 @@ mod test_reporting { { use ven_pretty::DocAllocator; + let src_lines: Vec<&str> = src.split('\n').collect(); + + let filename = filename_from_string(r"\code\proj\Main.roc"); + match infer_expr_help(src) { - Err(fail) => todo!(), - Ok((type_problems, can_problems, mono_problems, home, interns)) => { - let src_lines: Vec<&str> = src.split('\n').collect(); + Err(parse_err) => { + let ParseErrOut { + fail, + home, + interns, + } = parse_err; + let alloc = RocDocAllocator::new(&src_lines, home, &interns); - let filename = filename_from_string(r"\code\proj\Main.roc"); + let doc = parse_problem(&alloc, filename, fail); + + callback(doc.pretty(&alloc).append(alloc.line()), buf) + } + Ok((type_problems, can_problems, mono_problems, home, interns)) => { let mut reports = Vec::new(); + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + for problem in can_problems { let report = can_problem(&alloc, filename.clone(), problem.clone()); reports.push(report); @@ -2690,17 +2703,9 @@ mod test_reporting { ), indoc!( r#" - -- SYNTAX PROBLEM -------------------------------------------------------------- - - The `a` type variable is not used in the `Foo` alias definition: - - 1 ┆ Foo a : [ Foo ] - ┆ ^ - - Roc does not allow unused type parameters! - - Hint: If you want an unused type parameter (a so-called "phantom - type"), read the guide section on phantom data. + -- PARSE PROBLEM --------------------------------------------------------------- + + Unexpected tokens in front of the `=` symbol: "# ), )