diff --git a/compiler/reporting/Cargo.toml b/compiler/reporting/Cargo.toml index f966effd10..a6b6b3da1a 100644 --- a/compiler/reporting/Cargo.toml +++ b/compiler/reporting/Cargo.toml @@ -19,6 +19,7 @@ inlinable_string = "0.1.0" im = "14" # im and im-rc should always have the same version! im-rc = "14" # im and im-rc should always have the same version! distance = "0.4.0" +bumpalo = { version = "3.2", features = ["collections"] } [dev-dependencies] roc_constrain = { path = "../constrain" } @@ -30,4 +31,3 @@ maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" quickcheck_macros = "0.8" -bumpalo = { version = "3.2", features = ["collections"] } diff --git a/compiler/reporting/src/report.rs b/compiler/reporting/src/report.rs index caa914c09a..4ea21d8b6b 100644 --- a/compiler/reporting/src/report.rs +++ b/compiler/reporting/src/report.rs @@ -1,4 +1,5 @@ use crate::report::ReportText::{BinOp, Concat, Module, Region, Value}; +use bumpalo::Bump; use roc_module::ident::TagName; use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_problem::can::PrecedenceProblem::BothNonAssociative; @@ -527,11 +528,12 @@ impl ReportText { src_lines: &[&str], interns: &Interns, ) { + let arena = Bump::new(); let alloc = BoxAllocator; let err_msg = ""; - self.pretty::<_>(&alloc, subs, home, src_lines, interns) + self.pretty::<_>(&alloc, &arena, subs, home, src_lines, interns) .1 .render_raw(70, &mut CiWrite::new(buf)) .expect(err_msg); @@ -547,11 +549,12 @@ impl ReportText { interns: &Interns, palette: &Palette, ) { + let arena = Bump::new(); let alloc = BoxAllocator; let err_msg = ""; - self.pretty::<_>(&alloc, subs, home, src_lines, interns) + self.pretty::<_>(&alloc, &arena, subs, home, src_lines, interns) .1 .render_raw(70, &mut ColorWrite::new(palette, buf)) .expect(err_msg); @@ -562,6 +565,7 @@ impl ReportText { pub fn pretty<'b, D>( self, alloc: &'b D, + arena: &'b Bump, subs: &mut Subs, home: ModuleId, src_lines: &'b [&'b str], @@ -656,14 +660,14 @@ impl ReportText { .append(alloc.hardline()) .append( error_type - .pretty(alloc, subs, home, src_lines, interns) + .pretty(alloc, arena, subs, home, src_lines, interns) .indent(4) .annotate(Annotation::TypeBlock), ) .append(alloc.hardline()), Indent(n, nested) => { - let rest = nested.pretty(alloc, subs, home, src_lines, interns); + let rest = nested.pretty(alloc, arena, subs, home, src_lines, interns); alloc.nil().append(rest).indent(n) } Docs(_) => { @@ -672,22 +676,22 @@ impl ReportText { Concat(report_texts) => alloc.concat( report_texts .into_iter() - .map(|rep| rep.pretty(alloc, subs, home, src_lines, interns)), + .map(|rep| rep.pretty(alloc, arena, subs, home, src_lines, interns)), ), Stack(report_texts) => alloc .intersperse( report_texts .into_iter() - .map(|rep| (rep.pretty(alloc, subs, home, src_lines, interns))), + .map(|rep| (rep.pretty(alloc, arena, subs, home, src_lines, interns))), alloc.hardline(), ) .append(alloc.hardline()), Intersperse { separator, items } => alloc.intersperse( items .into_iter() - .map(|rep| (rep.pretty(alloc, subs, home, src_lines, interns))) + .map(|rep| (rep.pretty(alloc, arena, subs, home, src_lines, interns))) .collect::>(), - separator.pretty(alloc, subs, home, src_lines, interns), + separator.pretty(alloc, arena, subs, home, src_lines, interns), ), BinOp(bin_op) => alloc.text(bin_op.to_string()).annotate(Annotation::BinOp), Region(region) => { @@ -780,12 +784,15 @@ impl ReportText { Name(ident) => alloc .text(format!("{}", ident.as_inline_str())) .annotate(Annotation::Symbol), - TypeProblem(problem) => Self::type_problem_to_pretty(alloc, home, interns, problem), + TypeProblem(problem) => { + Self::type_problem_to_pretty(alloc, arena, home, interns, problem) + } } } fn type_problem_to_pretty<'b, D>( alloc: &'b D, + arena: &'b Bump, home: ModuleId, interns: &Interns, problem: crate::type_error::Problem, @@ -827,6 +834,26 @@ impl ReportText { } } } + FieldsMissing(missing) => match missing.split_last() { + None => alloc.nil(), + Some((f1, [])) => Self::hint(alloc) + .append(alloc.reflow("Looks like the ")) + .append(f1.as_str().to_owned()) + .append(alloc.reflow(" field is missing.")), + Some((last, init)) => { + let separator = alloc.reflow(", "); + + Self::hint(alloc) + .append(alloc.reflow("Looks like the ")) + .append( + alloc + .intersperse(init.iter().map(|v| v.as_str().to_owned()), separator), + ) + .append(alloc.reflow(" and ")) + .append(alloc.text(last.as_str().to_owned())) + .append(alloc.reflow(" fields are missing.")) + } + }, TagTypo(typo, possibilities_tn) => { let possibilities = possibilities_tn .into_iter() @@ -860,6 +887,59 @@ impl ReportText { } } } + ArityMismatch(found, expected) => { + let line = if found < expected { + format!( + "It looks like it takes too few arguments. I was expecting {} more.", + expected - found + ) + } else { + format!( + "It looks like it takes too many arguments. I'm seeing {} extra.", + found - expected + ) + }; + + Self::hint(alloc).append(line) + } + + BadRigidVar(x, tipe) => { + use ErrorType::*; + + let bad_rigid_var = |name: &str, a_thing| { + let text = format!( + r#"The type annotation uses the type variable `{}` to say that this definition can produce any type of value. But in the body I see that it will only produce {} of a single specific type. Maybe change the type annotation to be more specific? Maybe change the code to be more general?"#, + name, a_thing + ); + + Self::hint(alloc).append(alloc.reflow(arena.alloc(text))) + }; + + let bad_double_rigid = |a, b| { + let text = format!( + r#"Your type annotation uses {} and {} as separate type variables. Your code seems to be saying they are the same though. Maybe they should be the same your type annotation? Maybe your code uses them in a weird way?"#, + a, b + ); + + Self::hint(alloc).append(alloc.reflow(arena.alloc(text))) + }; + + match tipe { + Infinite | Error | FlexVar(_) => alloc.nil(), + RigidVar(y) => bad_double_rigid(x.as_str(), y.as_str()), + Function(_, _) => bad_rigid_var(x.as_str(), "a function value"), + Record(_, _) => bad_rigid_var(x.as_str(), "a record value"), + TagUnion(_, _) | RecursiveTagUnion(_, _, _) => { + bad_rigid_var(x.as_str(), "a tag value") + } + Alias(symbol, _, _) | Type(symbol, _) => bad_rigid_var( + x.as_str(), + &format!("a {} value", symbol.ident_string(interns)), + ), + Boolean(_) => bad_rigid_var(x.as_str(), "a uniqueness attribute value"), + } + } + _ => todo!(), } } diff --git a/compiler/reporting/src/type_error.rs b/compiler/reporting/src/type_error.rs index aba3a37ca9..a47a1f853b 100644 --- a/compiler/reporting/src/type_error.rs +++ b/compiler/reporting/src/type_error.rs @@ -1143,15 +1143,22 @@ fn to_diff(parens: Parens, type1: &ErrorType, type2: &ErrorType) -> Diff { + pair => { // We hit none of the specific cases where we give more detailed information let left = to_doc(Parens::Unnecessary, type1); let right = to_doc(Parens::Unnecessary, type2); + let problems = match pair { + (RigidVar(x), other) | (other, RigidVar(x)) => { + vec![Problem::BadRigidVar(x.clone(), other.clone())] + } + _ => vec![], + }; + Diff { left, right, - status: Status::Similar, + status: Status::Different(problems), } } } @@ -1200,8 +1207,8 @@ fn diff_record( let diff = to_diff(Parens::Unnecessary, t1, t2); Diff { - left: (plain_text(field.as_str()), diff.left), - right: (plain_text(field.as_str()), diff.right), + left: (field.clone(), plain_text(field.as_str()), diff.left), + right: (field.clone(), plain_text(field.as_str()), diff.right), status: diff.status, } }; @@ -1262,7 +1269,7 @@ fn diff_record( let ext_diff = ext_to_diff(ext1, ext2); - let mut fields_diff: Diff> = Diff { + let mut fields_diff: Diff> = Diff { left: vec![], right: vec![], status: Status::Similar, @@ -1275,13 +1282,31 @@ fn diff_record( } if !all_fields_shared { - fields_diff.left.extend(left.map(|(_, x, y)| (x, y))); - fields_diff.right.extend(right.map(|(_, x, y)| (x, y))); + fields_diff.left.extend(left); + fields_diff.right.extend(right); fields_diff.status.merge(Status::Different(vec![])); } - let doc1 = report_text::record(fields_diff.left, ext_diff.left); - let doc2 = report_text::record(fields_diff.right, ext_diff.right); + // sort fields for display + fields_diff.left.sort_by(|a, b| a.0.cmp(&b.0)); + fields_diff.right.sort_by(|a, b| a.0.cmp(&b.0)); + + let doc1 = report_text::record( + fields_diff + .left + .into_iter() + .map(|(_, b, c)| (b, c)) + .collect(), + ext_diff.left, + ); + let doc2 = report_text::record( + fields_diff + .right + .into_iter() + .map(|(_, b, c)| (b, c)) + .collect(), + ext_diff.right, + ); fields_diff.status.merge(status); diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index 51f6f3bdc4..20c50cedd8 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -1624,4 +1624,147 @@ mod test_reporting { ), ) } + + #[test] + fn missing_fields() { + report_problem_as( + indoc!( + r#" + x : { a : Int, b : Float, c : Bool } + x = { b: 4.0 } + + x + "# + ), + indoc!( + r#" + Something is off with the body of the `x` definition: + + 2 ┆ x = { b: 4.0 } + ┆ ^^^^^^^^^^ + + The body is a record of type: + + { b : Float } + + But the type annotation on `x` says it should be: + + { a : Int, b : Float, c : Bool } + + + Hint: Looks like the c and a fields are missing. + "# + ), + ) + } + + #[test] + fn bad_double_rigid() { + report_problem_as( + indoc!( + r#" + f : a, b -> a + f = \x, y -> if True then x else y + + f + "# + ), + indoc!( + r#" + Something is off with the body of the `f` definition: + + 2 ┆ f = \x, y -> if True then x else y + ┆ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The body is an anonymous function of type: + + a, a -> a + + But the type annotation on `f` says it should be: + + a, b -> a + + + Hint: Your type annotation uses a and b as separate type variables. + Your code seems to be saying they are the same though. Maybe they + should be the same your type annotation? Maybe your code uses them in + a weird way? + "# + ), + ) + } + + #[test] + fn bad_rigid_function() { + report_problem_as( + indoc!( + r#" + f : Bool -> msg + f = \_ -> Foo + + f + "# + ), + indoc!( + r#" + Something is off with the body of the `f` definition: + + 2 ┆ f = \_ -> Foo + ┆ ^^^^^^^^^ + + The body is an anonymous function of type: + + Bool -> [ Foo ]a + + But the type annotation on `f` says it should be: + + Bool -> msg + + + Hint: The type annotation uses the type variable `msg` to say that + this definition can produce any type of value. But in the body I see + that it will only produce a tag value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "# + ), + ) + } + + #[test] + fn bad_rigid_value() { + report_problem_as( + indoc!( + r#" + f : msg + f = 0x3 + + f + "# + ), + indoc!( + r#" + Something is off with the body of the `f` definition: + + 2 ┆ f = 0x3 + ┆ ^^^ + + The body is an integer of type: + + Int + + But the type annotation on `f` says it should be: + + msg + + + Hint: The type annotation uses the type variable `msg` to say that + this definition can produce any type of value. But in the body I see + that it will only produce a Int value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "# + ), + ) + } }