diff --git a/compiler/collections/src/all.rs b/compiler/collections/src/all.rs index de92d6cf08..858886b986 100644 --- a/compiler/collections/src/all.rs +++ b/compiler/collections/src/all.rs @@ -93,3 +93,40 @@ where map } + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Index(usize); + +impl Index { + pub const FIRST: Self = Index(0); + + pub fn zero_based(i: usize) -> Self { + Index(i) + } + + pub fn one_based(i: usize) -> Self { + Index(i - 1) + } + + pub fn ordinal(self) -> std::string::String { + int_to_ordinal(self.0 + 1) + } +} + +fn int_to_ordinal(number: usize) -> std::string::String { + // NOTE: one-based + let remainder10 = number % 10; + let remainder100 = number % 100; + + let ending = match remainder100 { + 11..=13 => "th", + _ => match remainder10 { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + }, + }; + + format!("{}{}", number, ending) +} diff --git a/compiler/constrain/src/expr.rs b/compiler/constrain/src/expr.rs index 922b32a86b..ae91dfbfd5 100644 --- a/compiler/constrain/src/expr.rs +++ b/compiler/constrain/src/expr.rs @@ -9,7 +9,7 @@ use roc_can::expected::PExpected; use roc_can::expr::Expr::{self, *}; use roc_can::expr::{Field, WhenBranch}; use roc_can::pattern::Pattern; -use roc_collections::all::{ImMap, SendMap}; +use roc_collections::all::{ImMap, Index, SendMap}; use roc_module::ident::Lowercase; use roc_module::symbol::{ModuleId, Symbol}; use roc_region::all::{Located, Region}; @@ -156,7 +156,7 @@ pub fn constrain_expr( cons.push(con); } - let fields_type = Type::Record(fields.clone(), Box::new(Type::Variable(*ext_var))); + let fields_type = Type::Record(fields, Box::new(Type::Variable(*ext_var))); let record_type = Type::Variable(*record_var); // NOTE from elm compiler: fields_type is separate so that Error propagates better @@ -211,7 +211,9 @@ pub fn constrain_expr( for (index, loc_elem) in loc_elems.iter().enumerate() { let elem_expected = ForReason( - Reason::ElemInList { index }, + Reason::ElemInList { + index: Index::zero_based(index), + }, list_elem_type.clone(), loc_elem.region, ); @@ -270,7 +272,7 @@ pub fn constrain_expr( let reason = Reason::FnArg { name: opt_symbol, - arg_index: index as u8, + arg_index: Index::zero_based(index), }; let expected_arg = ForReason(reason, arg_type.clone(), region); let arg_con = constrain_expr(env, loc_arg.region, &loc_arg.value, expected_arg); @@ -401,7 +403,10 @@ pub fn constrain_expr( FromAnnotation( name.clone(), arity, - AnnotationSource::TypedIfBranch(index + 1), + AnnotationSource::TypedIfBranch { + index: Index::zero_based(index), + num_branches: branches.len(), + }, tipe.clone(), ), ); @@ -416,7 +421,10 @@ pub fn constrain_expr( FromAnnotation( name, arity, - AnnotationSource::TypedIfBranch(branches.len() + 1), + AnnotationSource::TypedIfBranch { + index: Index::zero_based(branches.len()), + num_branches: branches.len(), + }, tipe.clone(), ), ); @@ -448,7 +456,7 @@ pub fn constrain_expr( &loc_body.value, ForReason( Reason::IfBranch { - index: index + 1, + index: Index::zero_based(index), total_branches: branches.len(), }, Type::Variable(*branch_var), @@ -465,7 +473,7 @@ pub fn constrain_expr( &final_else.value, ForReason( Reason::IfBranch { - index: branches.len() + 1, + index: Index::zero_based(branches.len()), total_branches: branches.len() + 1, }, Type::Variable(*branch_var), @@ -521,14 +529,16 @@ pub fn constrain_expr( when_branch.value.region, when_branch, PExpected::ForReason( - PReason::WhenMatch { index }, + PReason::WhenMatch { + index: Index::zero_based(index), + }, cond_type.clone(), Region::across_all(when_branch.patterns.iter().map(|v| &v.region)), ), FromAnnotation( name.clone(), *arity, - TypedWhenBranch(index + 1), + TypedWhenBranch(Index::zero_based(index)), typ.clone(), ), ); @@ -547,12 +557,16 @@ pub fn constrain_expr( region, when_branch, PExpected::ForReason( - PReason::WhenMatch { index }, + PReason::WhenMatch { + index: Index::zero_based(index), + }, cond_type.clone(), Region::across_all(when_branch.patterns.iter().map(|v| &v.region)), ), ForReason( - Reason::WhenBranch { index }, + Reason::WhenBranch { + index: Index::zero_based(index), + }, branch_type.clone(), when_branch.value.region, ), diff --git a/compiler/constrain/src/pattern.rs b/compiler/constrain/src/pattern.rs index b182391ea1..332104561e 100644 --- a/compiler/constrain/src/pattern.rs +++ b/compiler/constrain/src/pattern.rs @@ -3,7 +3,7 @@ use roc_can::constraint::Constraint; use roc_can::expected::{Expected, PExpected}; use roc_can::pattern::Pattern::{self, *}; use roc_can::pattern::RecordDestruct; -use roc_collections::all::SendMap; +use roc_collections::all::{Index, SendMap}; use roc_module::ident::Lowercase; use roc_module::symbol::Symbol; use roc_region::all::{Located, Region}; @@ -256,7 +256,7 @@ pub fn constrain_pattern( let expected = PExpected::ForReason( PReason::TagArg { tag_name: tag_name.clone(), - index, + index: Index::zero_based(index), }, pattern_type, region, diff --git a/compiler/constrain/src/uniq.rs b/compiler/constrain/src/uniq.rs index d1dae4ad23..b83b1edc5a 100644 --- a/compiler/constrain/src/uniq.rs +++ b/compiler/constrain/src/uniq.rs @@ -6,7 +6,7 @@ use roc_can::def::{Declaration, Def}; use roc_can::expected::{Expected, PExpected}; use roc_can::expr::{Expr, Field, WhenBranch}; use roc_can::pattern::{Pattern, RecordDestruct}; -use roc_collections::all::{ImMap, ImSet, SendMap}; +use roc_collections::all::{ImMap, ImSet, Index, SendMap}; use roc_module::ident::{Ident, Lowercase}; use roc_module::symbol::{ModuleId, Symbol}; use roc_region::all::{Located, Region}; @@ -598,7 +598,9 @@ pub fn constrain_expr( for (index, loc_elem) in loc_elems.iter().enumerate() { let elem_expected = Expected::ForReason( - Reason::ElemInList { index }, + Reason::ElemInList { + index: Index::zero_based(index), + }, entry_type.clone(), region, ); @@ -755,7 +757,7 @@ pub fn constrain_expr( let reason = Reason::FnArg { name: opt_symbol, - arg_index: index as u8, + arg_index: Index::zero_based(index), }; let expected_arg = Expected::ForReason(reason, arg_type.clone(), region); @@ -936,7 +938,10 @@ pub fn constrain_expr( Expected::FromAnnotation( name.clone(), arity, - AnnotationSource::TypedIfBranch(index + 1), + AnnotationSource::TypedIfBranch { + index: Index::zero_based(index), + num_branches: branches.len(), + }, tipe.clone(), ), ); @@ -954,7 +959,10 @@ pub fn constrain_expr( Expected::FromAnnotation( name, arity, - AnnotationSource::TypedIfBranch(branches.len() + 1), + AnnotationSource::TypedIfBranch { + index: Index::zero_based(branches.len()), + num_branches: branches.len(), + }, tipe.clone(), ), ); @@ -1003,7 +1011,7 @@ pub fn constrain_expr( &loc_body.value, Expected::ForReason( Reason::IfBranch { - index: index + 1, + index: Index::zero_based(index), total_branches: branches.len(), }, Type::Variable(*branch_var), @@ -1023,7 +1031,7 @@ pub fn constrain_expr( &final_else.value, Expected::ForReason( Reason::IfBranch { - index: branches.len() + 1, + index: Index::zero_based(branches.len()), total_branches: branches.len(), }, Type::Variable(*branch_var), @@ -1085,14 +1093,16 @@ pub fn constrain_expr( region, when_branch, PExpected::ForReason( - PReason::WhenMatch { index }, + PReason::WhenMatch { + index: Index::zero_based(index), + }, cond_type.clone(), region, ), Expected::FromAnnotation( name.clone(), *arity, - TypedWhenBranch(index), + TypedWhenBranch(Index::zero_based(index)), typ.clone(), ), ); @@ -1118,12 +1128,16 @@ pub fn constrain_expr( region, when_branch, PExpected::ForReason( - PReason::WhenMatch { index }, + PReason::WhenMatch { + index: Index::zero_based(index), + }, cond_type.clone(), region, ), Expected::ForReason( - Reason::WhenBranch { index }, + Reason::WhenBranch { + index: Index::zero_based(index), + }, branch_type.clone(), region, ), @@ -1176,7 +1190,7 @@ pub fn constrain_expr( let fields_type = attr_type( Bool::variable(uniq_var), - Type::Record(fields.clone(), Box::new(Type::Variable(*ext_var))), + Type::Record(fields, Box::new(Type::Variable(*ext_var))), ); let record_type = Type::Variable(*record_var); diff --git a/compiler/module/src/symbol.rs b/compiler/module/src/symbol.rs index 1d593916c1..dc8bdd1084 100644 --- a/compiler/module/src/symbol.rs +++ b/compiler/module/src/symbol.rs @@ -612,6 +612,7 @@ define_builtins! { 8 FLOAT_ADD: "#add" 9 FLOAT_SUB: "#sub" 10 FLOAT_EQ: "#eq" + 11 FLOAT_ROUND: "round" } 4 BOOL: "Bool" => { 0 BOOL_BOOL: "Bool" imported // the Bool.Bool type alias diff --git a/compiler/reporting/src/type_error.rs b/compiler/reporting/src/type_error.rs index 4516d79b9d..36a90d3b37 100644 --- a/compiler/reporting/src/type_error.rs +++ b/compiler/reporting/src/type_error.rs @@ -1,5 +1,5 @@ use roc_can::expected::{Expected, PExpected}; -use roc_collections::all::{MutSet, SendMap}; +use roc_collections::all::{Index, MutSet, SendMap}; use roc_module::ident::{Lowercase, TagName}; use roc_module::symbol::Symbol; use roc_solve::solve; @@ -32,24 +32,6 @@ pub fn type_problem<'b>( } } -fn int_to_ordinal(number: usize) -> String { - // NOTE: one-based - let remainder10 = number % 10; - let remainder100 = number % 100; - - let ending = match remainder100 { - 11..=13 => "th", - _ => match remainder10 { - 1 => "st", - 2 => "nd", - 3 => "rd", - _ => "th", - }, - }; - - format!("{}{}", number, ending) -} - #[allow(clippy::too_many_arguments)] fn report_mismatch<'b>( alloc: &'b RocDocAllocator<'b>, @@ -162,22 +144,34 @@ fn to_expr_report<'b>( let (the_name_text, on_name_text) = match pattern_to_doc(alloc, &name.value) { Some(doc) => ( - alloc.concat(vec![alloc.text("the "), doc.clone()]), - alloc.concat(vec![alloc.text(" on "), doc]), + alloc.concat(vec![alloc.reflow("the "), doc.clone()]), + alloc.concat(vec![alloc.reflow(" on "), doc]), ), None => (alloc.text("this"), alloc.nil()), }; - // TODO special-case 2-branch if let thing = match annotation_source { - TypedIfBranch(index) => alloc.concat(vec![ - alloc.string(format!("{}", int_to_ordinal(index))), + TypedIfBranch { + index, + num_branches, + } if num_branches == 2 => alloc.concat(vec![ + alloc.keyword(if index == Index::FIRST { + "then" + } else { + "else" + }), + alloc.reflow(" branch of this "), + alloc.keyword("if"), + alloc.text(" expression:"), + ]), + TypedIfBranch { index, .. } => alloc.concat(vec![ + alloc.string(index.ordinal()), alloc.reflow(" branch of this "), alloc.keyword("if"), alloc.text(" expression:"), ]), TypedWhenBranch(index) => alloc.concat(vec![ - alloc.string(format!("{}", int_to_ordinal(index))), + alloc.string(index.ordinal()), alloc.reflow(" branch of this "), alloc.keyword("when"), alloc.text(" expression:"), @@ -190,8 +184,8 @@ fn to_expr_report<'b>( }; let it_is = match annotation_source { - TypedIfBranch(index) => format!("The {} branch is", int_to_ordinal(index)), - TypedWhenBranch(index) => format!("The {} branch is", int_to_ordinal(index)), + TypedIfBranch { index, .. } => format!("The {} branch is", index.ordinal()), + TypedWhenBranch(index) => format!("The {} branch is", index.ordinal()), TypedBody => "The body is".into(), }; @@ -325,39 +319,7 @@ fn to_expr_report<'b>( alloc.text(" to have the same type!"), ])), ), - _ => { - let ith = int_to_ordinal(index); - - report_mismatch( - alloc, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.concat(vec![ - alloc.reflow("The "), - alloc.string(format!("{}", ith)), - alloc.reflow(" branch of this "), - alloc.keyword("if"), - alloc.reflow(" does not match all the previous branches:"), - ]), - alloc.string(format!("The {} branch is", ith)), - alloc.reflow("But all the previous branches have type:"), - Some(alloc.concat(vec![ - alloc.reflow("I need all branches in an "), - alloc.keyword("if"), - alloc.reflow(" to have the same type!"), - ])), - ) - } - }, - Reason::WhenBranch { index } => { - // NOTE: is 0-based - let ith = int_to_ordinal(index + 1); - - report_mismatch( + _ => report_mismatch( alloc, filename, &category, @@ -367,28 +329,49 @@ fn to_expr_report<'b>( Some(expr_region), alloc.concat(vec![ alloc.reflow("The "), - alloc.string(format!("{}", ith)), + alloc.string(index.ordinal()), alloc.reflow(" branch of this "), - alloc.keyword("when"), + alloc.keyword("if"), alloc.reflow(" does not match all the previous branches:"), ]), - alloc.concat(vec![ - alloc.reflow("The "), - alloc.string(format!("{}", ith)), - alloc.reflow(" branch is"), - ]), + alloc.string(format!("The {} branch is", index.ordinal())), alloc.reflow("But all the previous branches have type:"), Some(alloc.concat(vec![ - alloc.reflow("I need all branches of a "), - alloc.keyword("when"), + alloc.reflow("I need all branches in an "), + alloc.keyword("if"), alloc.reflow(" to have the same type!"), ])), - ) - } + ), + }, + Reason::WhenBranch { index } => report_mismatch( + alloc, + filename, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.concat(vec![ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" branch of this "), + alloc.keyword("when"), + alloc.reflow(" does not match all the previous branches:"), + ]), + alloc.concat(vec![ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" branch is"), + ]), + alloc.reflow("But all the previous branches have type:"), + Some(alloc.concat(vec![ + alloc.reflow("I need all branches of a "), + alloc.keyword("when"), + alloc.reflow(" to have the same type!"), + ])), + ), Reason::ElemInList { index } => { - // NOTE: is 0-based - - let ith = int_to_ordinal(index + 1); + let ith = index.ordinal(); report_mismatch( alloc, @@ -438,13 +421,9 @@ fn to_expr_report<'b>( let expected_set: MutSet<_> = expected_fields.keys().cloned().collect(); let actual_set: MutSet<_> = actual_fields.keys().cloned().collect(); - let diff = expected_set.difference(&actual_set); + let mut diff = expected_set.difference(&actual_set); - match diff - .into_iter() - .next() - .and_then(|k| Some((k, expected_fields.get(k)?))) - { + match diff.next().and_then(|k| Some((k, expected_fields.get(k)?))) { None => report_mismatch( alloc, filename, @@ -464,7 +443,7 @@ fn to_expr_report<'b>( ), Some((field, field_region)) => { let r_doc = alloc.symbol_unqualified(symbol); - let f_doc = alloc.record_field(field.clone().clone()); + let f_doc = alloc.record_field(field.clone()); let header = alloc.concat(vec![ alloc.reflow("The "), @@ -632,7 +611,7 @@ fn to_expr_report<'b>( } }, Reason::FnArg { name, arg_index } => { - let ith = int_to_ordinal(arg_index as usize + 1); + let ith = arg_index.ordinal(); let this_function = match name { None => alloc.text("this function"), @@ -661,17 +640,12 @@ fn to_expr_report<'b>( None, ) } - other => { - // NamedFnArg(String /* function name */, u8 /* arg index */), - // AnonymousFnCall { arity: u8 }, - // NamedFnCall(String /* function name */, u8 /* arity */), - // BinOpArg(BinOp, ArgSide), - // BinOpRet(BinOp), - // FloatLiteral, - // IntLiteral, - // NumLiteral, - // InterpolatedStringVar, - todo!("I don't have a message yet for reason {:?}", other) + Reason::FloatLiteral | Reason::IntLiteral | Reason::NumLiteral => { + unreachable!("I don't think these can be reached") + } + + Reason::InterpolatedStringVar => { + unimplemented!("string interpolation is not implemented yet") } }, } @@ -836,7 +810,7 @@ fn to_pattern_report<'b>( PExpected::ForReason(reason, expected_type, region) => match reason { PReason::WhenMatch { index } => { - if index == 0 { + if index == Index::FIRST { let doc = alloc.stack(vec![ alloc .text("The 1st pattern in this ") @@ -871,10 +845,7 @@ fn to_pattern_report<'b>( } else { let doc = alloc.stack(vec![ alloc - .string(format!( - "The {} pattern in this ", - int_to_ordinal(index + 1) - )) + .string(format!("The {} pattern in this ", index.ordinal())) .append(alloc.keyword("when")) .append(alloc.text(" does not match the previous ones:")), alloc.region(region), @@ -886,7 +857,7 @@ fn to_pattern_report<'b>( alloc, alloc.string(format!( "The {} pattern is trying to match", - int_to_ordinal(index + 1) + index.ordinal() )), &category, ), @@ -902,11 +873,8 @@ fn to_pattern_report<'b>( } } } - PReason::TagArg { .. } => { - panic!("I didn't think this could trigger. Please tell Folkert about it!") - } - PReason::PatternGuard => { - todo!("Blocked on https://github.com/rtfeldman/roc/issues/304") + PReason::TagArg { .. } | PReason::PatternGuard => { + unreachable!("I didn't think this could trigger. Please tell Folkert about it!") } }, } @@ -943,19 +911,19 @@ fn add_pattern_category<'b>( use PatternCategory::*; let rest = match category { - Record => alloc.text(" record values of type:"), - EmptyRecord => alloc.text(" an empty record:"), - PatternGuard => alloc.text(" a pattern guard of type:"), - Set => alloc.text(" sets of type:"), - Map => alloc.text(" maps of type:"), + Record => alloc.reflow(" record values of type:"), + EmptyRecord => alloc.reflow(" an empty record:"), + PatternGuard => alloc.reflow(" a pattern guard of type:"), + Set => alloc.reflow(" sets of type:"), + Map => alloc.reflow(" maps of type:"), Ctor(tag_name) => alloc.concat(vec![ alloc.tag_name(tag_name.clone()), - alloc.text(" values of type:"), + alloc.reflow(" values of type:"), ]), - Str => alloc.text(" strings:"), - Num => alloc.text(" numbers:"), - Int => alloc.text(" integers:"), - Float => alloc.text(" floats"), + Str => alloc.reflow(" strings:"), + Num => alloc.reflow(" numbers:"), + Int => alloc.reflow(" integers:"), + Float => alloc.reflow(" floats"), }; alloc.concat(vec![i_am_trying_to_match, rest]) @@ -972,7 +940,6 @@ fn to_circular_report<'b>( title: "CIRCULAR TYPE".to_string(), filename, doc: { - let line = r#"Here is my best effort at writing down the type. You will see ∞ for parts of the type that repeat something already printed out infinitely."#; alloc.stack(vec![ alloc .reflow("I'm inferring a weird self-referential type for ") @@ -980,7 +947,11 @@ fn to_circular_report<'b>( .append(alloc.text(":")), alloc.region(region), alloc.stack(vec![ - alloc.reflow(line), + alloc.reflow( + "Here is my best effort at writing down the type. \ + You will see ∞ for parts of the type that repeat \ + something already printed out infinitely.", + ), alloc.type_block(to_doc(alloc, Parens::Unnecessary, overall_type)), ]), ]) @@ -994,8 +965,6 @@ pub enum Problem { ArityMismatch(usize, usize), FieldTypo(Lowercase, Vec), FieldsMissing(Vec), - - // TODO maybe these should include the arguments too? TagTypo(TagName, Vec), TagsMissing(Vec), BadRigidVar(Lowercase, ErrorType), @@ -1375,8 +1344,34 @@ fn to_diff<'b>( let left = to_doc(alloc, Parens::Unnecessary, type1); let right = to_doc(alloc, Parens::Unnecessary, type2); + let is_int = |t: &ErrorType| match t { + ErrorType::Type(Symbol::INT_INT, _) => true, + ErrorType::Alias(Symbol::INT_INT, _, _) => true, + + ErrorType::Type(Symbol::NUM_NUM, args) => match &args.get(0) { + Some(ErrorType::Type(Symbol::INT_INTEGER, _)) => true, + Some(ErrorType::Alias(Symbol::INT_INTEGER, _, _)) => true, + _ => false, + }, + ErrorType::Alias(Symbol::NUM_NUM, args, _) => match &args.get(0) { + Some((_, ErrorType::Type(Symbol::INT_INTEGER, _))) => true, + Some((_, ErrorType::Alias(Symbol::INT_INTEGER, _, _))) => true, + _ => false, + }, + _ => false, + }; + let is_float = |t: &ErrorType| match t { + ErrorType::Type(Symbol::FLOAT_FLOAT, _) => true, + ErrorType::Alias(Symbol::FLOAT_FLOAT, _, _) => true, + + _ => false, + }; + let problems = match pair { (RigidVar(x), other) | (other, RigidVar(x)) => vec![Problem::BadRigidVar(x, other)], + (a, b) if (is_int(&a) && is_float(&b)) || (is_float(&a) && is_int(&b)) => { + vec![Problem::IntFloat] + } _ => vec![], }; @@ -2148,7 +2143,58 @@ fn type_problem_to_pretty<'b>( Boolean(_) => bad_rigid_var(x, alloc.reflow("a uniqueness attribute value")), } } + IntFloat => alloc.hint().append(alloc.concat(vec![ + alloc.reflow("Convert between "), + alloc.type_str("Int"), + alloc.reflow(" and "), + alloc.type_str("Float"), + alloc.reflow(" with "), + alloc.symbol_qualified(Symbol::NUM_TO_FLOAT), + alloc.reflow(" and "), + alloc.symbol_qualified(Symbol::FLOAT_ROUND), + alloc.reflow("."), + ])), - _ => todo!(), + TagsMissing(missing) => match missing.split_last() { + None => alloc.nil(), + Some((f1, [])) => { + let hint1 = alloc + .hint() + .append(alloc.reflow("Looks like a closed tag union does not have the ")) + .append(alloc.tag_name(f1.clone())) + .append(alloc.reflow(" tag.")); + + let hint2 = alloc.hint().append(alloc.reflow( + "Closed tag unions can't grow, \ + because that might change the size in memory. \ + Can you use an open tag union?", + )); + + alloc.stack(vec![hint1, hint2]) + } + + Some((last, init)) => { + let separator = alloc.reflow(", "); + + let hint1 = alloc + .hint() + .append(alloc.reflow("Looks like a closed tag union does not have the ")) + .append( + alloc + .intersperse(init.iter().map(|v| alloc.tag_name(v.clone())), separator), + ) + .append(alloc.reflow(" and ")) + .append(alloc.tag_name(last.clone())) + .append(alloc.reflow(" tags.")); + + let hint2 = alloc.hint().append(alloc.reflow( + "Closed tag unions can't grow, \ + because that might change the size in memory. \ + Can you use an open tag union?", + )); + + alloc.stack(vec![hint1, hint2]) + } + }, } } diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index 71e90f29d3..0118e46e57 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -1067,6 +1067,8 @@ mod test_reporting { But the type annotation on `x` says it should be: Int + + Hint: Convert between Int and Float with `Num.toFloat` and `Float.round`. "# ), ) @@ -1101,6 +1103,8 @@ mod test_reporting { But the type annotation on `x` says it should be: Int + + Hint: Convert between Int and Float with `Num.toFloat` and `Float.round`. "# ), ) @@ -1133,6 +1137,8 @@ mod test_reporting { But the type annotation on `x` says it should be: Int -> Int + + Hint: Convert between Int and Float with `Num.toFloat` and `Float.round`. "# ), ) @@ -1461,6 +1467,8 @@ mod test_reporting { But the type annotation says it should be: { x : Int } + + Hint: Convert between Int and Float with `Num.toFloat` and `Float.round`. "# ), ) @@ -1883,4 +1891,155 @@ mod test_reporting { ), ) } + + #[test] + fn plus_on_str() { + report_problem_as( + indoc!( + r#" + 0x4 + "foo" + "# + ), + // TODO also suggest fields with the correct type + indoc!( + r#" + -- TYPE MISMATCH --------------------------------------------------------------- + + The 2nd argument to `add` is not what I expect: + + 1 ┆ 0x4 + "foo" + ┆ ^^^^^ + + This argument is a string of type: + + Str + + But `add` needs the 2nd argument to be: + + Num Integer + "# + ), + ) + } + + #[test] + fn int_float() { + report_problem_as( + indoc!( + r#" + 0x4 + 3.14 + "# + ), + indoc!( + r#" + -- TYPE MISMATCH --------------------------------------------------------------- + + The 2nd argument to `add` is not what I expect: + + 1 ┆ 0x4 + 3.14 + ┆ ^^^^ + + This argument is a float of type: + + Float + + But `add` needs the 2nd argument to be: + + Num Integer + + Hint: Convert between Int and Float with `Num.toFloat` and `Float.round`. + "# + ), + ) + } + + #[test] + fn tag_missing() { + report_problem_as( + indoc!( + r#" + f : [ A ] -> [ A, B ] + f = \a -> a + + f + "# + ), + indoc!( + r#" + -- TYPE MISMATCH --------------------------------------------------------------- + + Something is off with the body of the `f` definition: + + 2 ┆ f = \a -> a + ┆ ^^^^^^^ + + The body is an anonymous function of type: + + [ A ] -> [ A ] + + But the type annotation on `f` says it should be: + + [ A ] -> [ A, B ] + + Hint: Looks like a closed tag union does not have the `B` tag. + + Hint: Closed tag unions can't grow, because that might change the size + in memory. Can you use an open tag union? + "# + ), + ) + } + + #[test] + fn tags_missing() { + report_problem_as( + indoc!( + r#" + f : [ A ] -> [ A, B, C ] + f = \a -> a + + f + "# + ), + indoc!( + r#" + -- TYPE MISMATCH --------------------------------------------------------------- + + Something is off with the body of the `f` definition: + + 2 ┆ f = \a -> a + ┆ ^^^^^^^ + + The body is an anonymous function of type: + + [ A ] -> [ A ] + + But the type annotation on `f` says it should be: + + [ A ] -> [ A, B, C ] + + Hint: Looks like a closed tag union does not have the `C` and `B` tags. + + Hint: Closed tag unions can't grow, because that might change the size + in memory. Can you use an open tag union? + "# + ), + ) + } + + #[test] + fn open_tag_union_can_grow() { + report_problem_as( + indoc!( + r#" + f : [ A ]* -> [ A, B, C ] + f = \a -> a + + f + "# + ), + // should not give errors + indoc!(""), + ) + } } diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs index bc64ff29c0..a205f3af4f 100644 --- a/compiler/types/src/types.rs +++ b/compiler/types/src/types.rs @@ -2,10 +2,9 @@ use crate::boolean_algebra; use crate::pretty_print::Parens; use crate::subs::{Subs, VarStore, Variable}; use inlinable_string::InlinableString; -use roc_collections::all::{union, ImMap, ImSet, MutMap, MutSet, SendMap}; +use roc_collections::all::{union, ImMap, ImSet, Index, MutMap, MutSet, SendMap}; use roc_module::ident::{Ident, Lowercase, TagName}; use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_parse::operator::{ArgSide, BinOp}; use roc_region::all::{Located, Region}; use std::fmt; @@ -598,33 +597,46 @@ pub struct RecordStructure { #[derive(Debug, Clone, PartialEq, Eq)] pub enum PReason { - WhenMatch { index: usize }, - TagArg { tag_name: TagName, index: usize }, + WhenMatch { index: Index }, + TagArg { tag_name: TagName, index: Index }, PatternGuard, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum AnnotationSource { - TypedIfBranch(usize /* index */), - TypedWhenBranch(usize /* index */), + TypedIfBranch { index: Index, num_branches: usize }, + TypedWhenBranch(Index), TypedBody, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Reason { - FnArg { name: Option, arg_index: u8 }, - FnCall { name: Option, arity: u8 }, - BinOpArg(BinOp, ArgSide), - BinOpRet(BinOp), + FnArg { + name: Option, + arg_index: Index, + }, + FnCall { + name: Option, + arity: u8, + }, + // BinOpArg(BinOp, ArgSide), + // BinOpRet(BinOp), FloatLiteral, IntLiteral, NumLiteral, InterpolatedStringVar, - WhenBranch { index: usize }, + WhenBranch { + index: Index, + }, WhenGuard, IfCondition, - IfBranch { index: usize, total_branches: usize }, - ElemInList { index: usize }, + IfBranch { + index: Index, + total_branches: usize, + }, + ElemInList { + index: Index, + }, RecordUpdateValue(Lowercase), RecordUpdateKeys(Symbol, SendMap), }