diff --git a/compiler/constrain/src/expr.rs b/compiler/constrain/src/expr.rs index 9627a0b021..183361cb56 100644 --- a/compiler/constrain/src/expr.rs +++ b/compiler/constrain/src/expr.rs @@ -16,7 +16,7 @@ use roc_region::all::{Located, Region}; use roc_types::subs::Variable; use roc_types::types::AnnotationSource::{self, *}; use roc_types::types::Type::{self, *}; -use roc_types::types::{Alias, Category, PReason, PatternCategory, Reason}; +use roc_types::types::{Alias, Category, PReason, Reason}; /// This is for constraining Defs #[derive(Default, Debug)] diff --git a/compiler/constrain/src/uniq.rs b/compiler/constrain/src/uniq.rs index fe4aa61697..bd759383a7 100644 --- a/compiler/constrain/src/uniq.rs +++ b/compiler/constrain/src/uniq.rs @@ -14,7 +14,7 @@ use roc_types::boolean_algebra::{Atom, Bool}; use roc_types::subs::{VarStore, Variable}; use roc_types::types::AnnotationSource::{self, *}; use roc_types::types::Type::{self, *}; -use roc_types::types::{Alias, Category, PReason, PatternCategory, Reason}; +use roc_types::types::{Alias, Category, PReason, Reason}; use roc_uniq::builtins::{attr_type, empty_list_type, list_type, str_type}; use roc_uniq::sharing::{self, Container, FieldAccess, Mark, Usage, VarUsage}; diff --git a/compiler/reporting/src/report.rs b/compiler/reporting/src/report.rs index 08bcde3a09..c34dbc52f9 100644 --- a/compiler/reporting/src/report.rs +++ b/compiler/reporting/src/report.rs @@ -1,12 +1,10 @@ -use crate::report::ReportText::{Batch, BinOp, Module, Region, Value}; -use roc_can::expected::{Expected, PExpected}; +use crate::report::ReportText::{BinOp, Concat, Module, Region, Value}; use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_problem::can::PrecedenceProblem::BothNonAssociative; use roc_problem::can::Problem; -use roc_solve::solve; use roc_types::pretty_print::content_to_string; -use roc_types::subs::{Content, Subs, Variable}; -use roc_types::types::{write_error_type, Category, ErrorType, PReason, PatternCategory, Reason}; +use roc_types::subs::{Content, Subs}; +use roc_types::types::{write_error_type, ErrorType}; use std::path::PathBuf; /// A textual report. @@ -72,208 +70,6 @@ impl Color { } } -pub fn type_problem(filename: PathBuf, problem: solve::TypeError) -> Report { - use solve::TypeError::*; - - match problem { - BadExpr(region, category, found, expected) => { - to_expr_report(filename, region, category, found, expected) - } - BadPattern(region, category, found, expected) => { - to_pattern_report(filename, region, category, found, expected) - } - CircularType(region, symbol, overall_type) => { - to_circular_report(filename, region, symbol, overall_type) - } - } -} - -fn type_in_focus(typ: ErrorType) -> ReportText { - ReportText::Batch(vec![ - newline(), - newline(), - plain_text(" "), - ReportText::ErrorType(typ), - newline(), - newline(), - ]) -} - -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(too_many_arguments)] -fn report_mismatch( - filename: PathBuf, - category: &Category, - found: ErrorType, - expected_type: ErrorType, - region: roc_region::all::Region, - opt_highlight: Option, - problem: &str, - this_is: &str, - instead_of: &str, - further_details: ReportText, -) -> Report { - use ReportText::*; - let lines = vec![ - plain_text(problem), - Region(region), - add_category(this_is, category), - type_in_focus(found), - plain_text(instead_of), - type_in_focus(expected_type), - further_details, - ]; - - Report { - filename, - text: Batch(lines), - } -} - -#[allow(too_many_arguments)] -fn report_bad_type( - filename: PathBuf, - category: &Category, - found: ErrorType, - expected_type: ErrorType, - region: roc_region::all::Region, - opt_highlight: Option, - problem: &str, - this_is: &str, - further_details: ReportText, -) -> Report { - use ReportText::*; - let lines = vec![ - plain_text(problem), - Region(region), - add_category(this_is, &category), - type_in_focus(found), - further_details, - ]; - - Report { - filename, - text: Batch(lines), - } -} - -fn to_expr_report( - filename: PathBuf, - expr_region: roc_region::all::Region, - category: Category, - found: ErrorType, - expected: Expected, -) -> Report { - use ReportText::*; - - match expected { - Expected::NoExpectation(expected_type) => todo!(), - Expected::FromAnnotation(name, arity, sub_context, expected_type) => todo!(), - Expected::ForReason(reason, expected_type, region) => { - match reason { - Reason::IfCondition => report_bad_type( - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - "This `if` condition does not evaluate to a boolean value, True or False.", - "It is", - Batch(vec![ - plain_text("But I need this `if` condition to be a "), - ReportText::Type(Content::Alias(Symbol::BOOL_BOOL, vec![], Variable::BOOL)), - plain_text(" value."), - newline(), - ]), - ), - Reason::IfBranch { index } => { - let ith = int_to_ordinal(index); - report_mismatch( - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - &format!( - "The {} branch of this `if` does not match all the previous branches:", - ith - ), - &format!("The {} branch is", ith), - "But all the previous branches result in", - Batch(vec![ /* TODO add hint */ ]), - ) - } - _ => todo!(), - } - } - } -} - -fn add_category(this_is: &str, category: &Category) -> ReportText { - use Category::*; - - let result = match category { - Str => format!("{} a string of type:", this_is), - _ => todo!(), - }; - - plain_text(&*result) -} - -fn to_pattern_report( - filename: PathBuf, - expr_region: roc_region::all::Region, - category: PatternCategory, - found: ErrorType, - expected: PExpected, -) -> Report { - use ReportText::*; - todo!() -} - -fn to_circular_report( - filename: PathBuf, - region: roc_region::all::Region, - symbol: Symbol, - overall_type: ErrorType, -) -> Report { - use ReportText::*; - - let lines = vec![ - plain_text("I'm inferring a weird self-referential type for "), - Value(symbol), - plain_text(":"), - Region(region), - plain_text("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."), - type_in_focus(overall_type), - /* TODO hint */ - ]; - - Report { - filename, - text: Batch(lines), - } -} - pub fn can_problem(filename: PathBuf, problem: Problem) -> Report { let mut texts = Vec::new(); @@ -352,7 +148,7 @@ pub fn can_problem(filename: PathBuf, problem: Problem) -> Report { Report { filename, - text: Batch(texts), + text: Concat(texts), } } @@ -387,7 +183,12 @@ pub enum ReportText { BinOp(roc_parse::operator::BinOp), /// Many ReportText that should be concatenated together. - Batch(Vec), + Concat(Vec), + + /// Many ReportText that each get separate lines + Stack(Vec), + + Indent(usize, Box), } pub fn plain_text(str: &str) -> ReportText { @@ -408,9 +209,8 @@ pub fn url(str: &str) -> ReportText { Url(Box::from(str)) } -#[allow(dead_code)] -fn newline() -> ReportText { - plain_text("\n") +pub fn with_indent(n: usize, report_text: ReportText) -> ReportText { + ReportText::Indent(n, Box::new(report_text)) } pub const RED_CODE: &str = "\u{001b}[31m"; @@ -473,6 +273,12 @@ fn white(str: &str) -> String { pub const RESET_CODE: &str = "\u{001b}[0m"; +struct CiEnv<'a> { + home: ModuleId, + src_lines: &'a [&'a str], + interns: &'a Interns, +} + impl ReportText { /// Render to CI console output, where no colors are available. pub fn render_ci( @@ -483,6 +289,16 @@ impl ReportText { src_lines: &[&str], interns: &Interns, ) { + let env = CiEnv { + home, + src_lines, + interns, + }; + + self.render_ci_help(&env, buf, subs, 0); + } + + fn render_ci_help(self, env: &CiEnv, buf: &mut String, subs: &mut Subs, indent: usize) { use ReportText::*; match self { @@ -499,25 +315,31 @@ impl ReportText { buf.push('>'); } Value(symbol) => { - if symbol.module_id() == home { + if symbol.module_id() == env.home { // Render it unqualified if it's in the current module. - buf.push_str(symbol.ident_string(interns)); + buf.push_str(symbol.ident_string(env.interns)); } else { - buf.push_str(symbol.module_string(interns)); + buf.push_str(symbol.module_string(env.interns)); buf.push('.'); - buf.push_str(symbol.ident_string(interns)); + buf.push_str(symbol.ident_string(env.interns)); } } Module(module_id) => { - buf.push_str(&interns.module_name(module_id)); + buf.push_str(&env.interns.module_name(module_id)); + } + Type(content) => { + buf.push_str(content_to_string(content, subs, env.home, env.interns).as_str()) + } + ErrorType(error_type) => { + buf.push('\n'); + buf.push_str(" ".repeat(indent).as_str()); + buf.push_str(&write_error_type(env.home, env.interns, error_type)); + buf.push('\n'); } - Type(content) => buf.push_str(content_to_string(content, subs, home, interns).as_str()), - ErrorType(error_type) => buf.push_str(&write_error_type(home, interns, error_type)), Region(region) => { buf.push('\n'); buf.push('\n'); - dbg!(region); // widest displayed line number let max_line_number_length = (region.end_line + 1).to_string().len(); @@ -535,11 +357,11 @@ impl ReportText { buf.push_str(line_number); buf.push_str(" ┆"); - let line = src_lines[i as usize]; + let line = env.src_lines[i as usize]; if !line.trim().is_empty() { buf.push_str(" "); - buf.push_str(src_lines[i as usize]); + buf.push_str(env.src_lines[i as usize]); } buf.push('\n'); @@ -566,11 +388,11 @@ impl ReportText { buf.push_str(line_number); buf.push_str(" ┆>"); - let line = src_lines[i as usize]; + let line = env.src_lines[i as usize]; if !line.trim().is_empty() { buf.push_str(" "); - buf.push_str(src_lines[i as usize]); + buf.push_str(env.src_lines[i as usize]); } if i != region.end_line { @@ -582,12 +404,27 @@ impl ReportText { buf.push('\n'); buf.push('\n'); } + Indent(n, nested) => { + nested.render_ci_help(env, buf, subs, indent + n); + } Docs(_) => { panic!("TODO implment docs"); } - Batch(report_texts) => { + Concat(report_texts) => { for report_text in report_texts { - report_text.render_ci(buf, subs, home, src_lines, interns); + report_text.render_ci_help(env, buf, subs, indent); + } + } + Stack(report_texts) => { + let mut it = report_texts.into_iter().peekable(); + + while let Some(report_text) = it.next() { + report_text.render_ci_help(env, buf, subs, indent); + + buf.push('\n'); + if it.peek().is_some() { + buf.push_str(" ".repeat(indent).as_str()); + } } } BinOp(bin_op) => { @@ -729,7 +566,11 @@ impl ReportText { buf.push('\n'); buf.push('\n'); } - Batch(report_texts) => { + Indent(n, nested) => { + buf.push_str(" ".repeat(n).as_str()); + nested.render_color_terminal(buf, subs, home, src_lines, interns, palette); + } + Concat(report_texts) => { for report_text in report_texts { report_text.render_color_terminal(buf, subs, home, src_lines, interns, palette); } diff --git a/compiler/reporting/src/type_error.rs b/compiler/reporting/src/type_error.rs new file mode 100644 index 0000000000..ae3fb16232 --- /dev/null +++ b/compiler/reporting/src/type_error.rs @@ -0,0 +1,264 @@ +use crate::report::{plain_text, with_indent, Report, ReportText}; +use roc_can::expected::{Expected, PExpected}; +use roc_module::symbol::Symbol; +use roc_solve::solve; +use roc_types::subs::{Content, Variable}; +use roc_types::types::{Category, ErrorType, PatternCategory, Reason}; +use std::path::PathBuf; + +pub fn type_problem(filename: PathBuf, problem: solve::TypeError) -> Report { + use solve::TypeError::*; + + match problem { + BadExpr(region, category, found, expected) => { + to_expr_report(filename, region, category, found, expected) + } + BadPattern(region, category, found, expected) => { + to_pattern_report(filename, region, category, found, expected) + } + CircularType(region, symbol, overall_type) => { + to_circular_report(filename, region, symbol, overall_type) + } + } +} + +fn type_in_focus(typ: ErrorType) -> ReportText { + ReportText::ErrorType(typ) +} + +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( + filename: PathBuf, + category: &Category, + found: ErrorType, + expected_type: ErrorType, + region: roc_region::all::Region, + _opt_highlight: Option, + problem: &str, + this_is: &str, + instead_of: &str, + further_details: ReportText, +) -> Report { + use ReportText::*; + let lines = vec![ + plain_text(problem), + Region(region), + type_comparison( + found, + expected_type, + add_category(this_is, category), + instead_of, + further_details, + ), + ]; + + Report { + filename, + text: Concat(lines), + } +} + +#[allow(clippy::too_many_arguments)] +fn report_bad_type( + filename: PathBuf, + category: &Category, + found: ErrorType, + expected_type: ErrorType, + region: roc_region::all::Region, + _opt_highlight: Option, + problem: &str, + this_is: &str, + further_details: ReportText, +) -> Report { + use ReportText::*; + let lines = vec![ + plain_text(problem), + Region(region), + lone_type( + found, + expected_type, + add_category(this_is, &category), + further_details, + ), + ]; + + Report { + filename, + text: Concat(lines), + } +} + +fn to_expr_report( + filename: PathBuf, + expr_region: roc_region::all::Region, + category: Category, + found: ErrorType, + expected: Expected, +) -> Report { + use ReportText::*; + + match expected { + Expected::NoExpectation(_expected_type) => todo!(), + Expected::FromAnnotation(_name, _arity, _sub_context, _expected_type) => todo!(), + Expected::ForReason(reason, expected_type, region) => { + match reason { + Reason::IfCondition => report_bad_type( + filename, + &category, + found, + expected_type, + region, + Some(expr_region), + "This `if` condition does not evaluate to a boolean value, True or False.", + "It is", + Concat(vec![ + plain_text("But I need this `if` condition to be a "), + ReportText::Type(Content::Alias(Symbol::BOOL_BOOL, vec![], Variable::BOOL)), + plain_text(" value."), + ]), + ), + Reason::IfBranch { index } => { + let ith = int_to_ordinal(index); + report_mismatch( + filename, + &category, + found, + expected_type, + region, + Some(expr_region), + &format!( + "The {} branch of this `if` does not match all the previous branches:", + ith + ), + &format!("The {} branch is", ith), + "But all the previous branches result in", + Concat(vec![ /* TODO add hint */ ]), + ) + } + _ => todo!(), + } + } + } +} + +pub enum Problem {} +pub struct Comparison { + actual: ReportText, + expected: ReportText, + problems: Vec, +} + +fn problems_to_hint(_problems: Vec) -> ReportText { + // TODO + ReportText::Concat(vec![]) +} + +fn to_comparison(actual: ErrorType, expected: ErrorType) -> Comparison { + // TODO make this do actual comparison + + Comparison { + actual: type_in_focus(actual), + expected: type_in_focus(expected), + problems: vec![], + } +} + +fn type_comparison( + actual: ErrorType, + expected: ErrorType, + i_am_seeing: ReportText, + instead_of: &str, + context_hints: ReportText, +) -> ReportText { + let comparison = to_comparison(actual, expected); + + ReportText::Stack(vec![ + i_am_seeing, + with_indent(4, comparison.actual), + plain_text(instead_of), + with_indent(4, comparison.expected), + context_hints, + problems_to_hint(comparison.problems), + ]) +} + +fn lone_type( + actual: ErrorType, + expected: ErrorType, + i_am_seeing: ReportText, + further_details: ReportText, +) -> ReportText { + let comparison = to_comparison(actual, expected); + + ReportText::Stack(vec![ + i_am_seeing, + with_indent(4, comparison.actual), + further_details, + problems_to_hint(comparison.problems), + ]) +} + +fn add_category(this_is: &str, category: &Category) -> ReportText { + use Category::*; + + let result = match category { + Str => format!("{} a string of type:", this_is), + _ => todo!(), + }; + + plain_text(&*result) +} + +fn to_pattern_report( + _filename: PathBuf, + _expr_region: roc_region::all::Region, + _category: PatternCategory, + _found: ErrorType, + _expected: PExpected, +) -> Report { + todo!() +} + +fn to_circular_report( + filename: PathBuf, + region: roc_region::all::Region, + symbol: Symbol, + overall_type: ErrorType, +) -> Report { + use ReportText::*; + + let lines = vec![ + plain_text("I'm inferring a weird self-referential type for "), + Value(symbol), + plain_text(":"), + Region(region), + Stack(vec![ + plain_text("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."), + with_indent(4, type_in_focus(overall_type)), + /* TODO hint */ + ]), + ]; + + Report { + filename, + text: Concat(lines), + } +} diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index 7a563fb181..0f431f569e 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -12,16 +12,17 @@ mod test_reporting { use crate::helpers::test_home; use roc_module::symbol::{Interns, ModuleId}; use roc_reporting::report::{ - can_problem, em_text, plain_text, type_problem, url, Report, ReportText, BLUE_CODE, - BOLD_CODE, CYAN_CODE, GREEN_CODE, MAGENTA_CODE, RED_CODE, RESET_CODE, TEST_PALETTE, - UNDERLINE_CODE, WHITE_CODE, YELLOW_CODE, + can_problem, em_text, plain_text, url, Report, ReportText, BLUE_CODE, BOLD_CODE, CYAN_CODE, + GREEN_CODE, MAGENTA_CODE, RED_CODE, RESET_CODE, TEST_PALETTE, 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_reporting::report::ReportText::{Batch, Module, Region, Type, Value}; + use roc_reporting::report::ReportText::{Concat, Module, Region, Type, Value}; use roc_solve::solve; use roc_types::subs::Content::{FlexVar, RigidVar, Structure}; use roc_types::subs::FlatType::EmptyRecord; @@ -276,7 +277,7 @@ mod test_reporting { report_texts.push(em_text("y")); report_renders_as( - to_simple_report(Batch(report_texts)), + to_simple_report(Concat(report_texts)), "Wait a second. There is a problem here. -> *y*", ); } @@ -566,7 +567,7 @@ mod test_reporting { report_texts.push(Type(Structure(EmptyRecord))); report_renders_in_color( - to_simple_report(Batch(report_texts)), + to_simple_report(Concat(report_texts)), "List {}", ); } @@ -775,6 +776,7 @@ mod test_reporting { Str But I need this `if` condition to be a Bool value. + "# ), ) @@ -803,6 +805,8 @@ mod test_reporting { Num a + + "# ), ) @@ -839,7 +843,7 @@ mod test_reporting { report_problem_as( indoc!( r#" - f = \x -> f [x] + f = \x -> f [x] f "# @@ -848,7 +852,7 @@ mod test_reporting { r#" I'm inferring a weird self-referential type for f: - 1 ┆ f = \x -> f [x] + 1 ┆ f = \x -> f [x] ┆ ^ 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. diff --git a/compiler/solve/src/solve.rs b/compiler/solve/src/solve.rs index 5424841408..3a513e6e10 100644 --- a/compiler/solve/src/solve.rs +++ b/compiler/solve/src/solve.rs @@ -217,6 +217,8 @@ fn solve( } Failure(vars, actual_type, expected_type) => { + introduce(subs, rank, pools, &vars); + let problem = TypeError::BadExpr( *region, Category::Lookup(*symbol), diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs index e34c05a319..d55f087f2f 100644 --- a/compiler/types/src/types.rs +++ b/compiler/types/src/types.rs @@ -817,14 +817,17 @@ fn write_error_type_help( } Record(fields, ext) => { buf.push('{'); + + for (label, content) in fields { + buf.push_str(label.as_str()); + buf.push_str(": "); + write_error_type_help(home, interns, content, buf, Parens::Unnecessary); + } + buf.push('}'); write_type_ext(ext, buf); } - Infinite => { - buf.push_str("∞"); - } - other => todo!("cannot format {:?} yet", other), } }