diff --git a/compiler/can/src/pattern.rs b/compiler/can/src/pattern.rs index 5c456ff32c..d068ce19cd 100644 --- a/compiler/can/src/pattern.rs +++ b/compiler/can/src/pattern.rs @@ -267,45 +267,26 @@ pub fn canonicalize_pattern<'a>( }; } RecordField(label, loc_guard) => { - match scope.introduce( - label.into(), - &env.exposed_ident_ids, - &mut env.ident_ids, - region, - ) { - Ok(symbol) => { - let can_guard = canonicalize_pattern( - env, - var_store, - scope, - pattern_type, - &loc_guard.value, - loc_guard.region, - ); + // a guard does not introduce the label into scope! + let symbol = scope.ignore(label.into(), &mut env.ident_ids); + let can_guard = canonicalize_pattern( + env, + var_store, + scope, + pattern_type, + &loc_guard.value, + loc_guard.region, + ); - destructs.push(Located { - region: loc_pattern.region, - value: RecordDestruct { - var: var_store.fresh(), - label: Lowercase::from(label), - symbol, - guard: Some((var_store.fresh(), can_guard)), - }, - }); - } - Err((original_region, shadow)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - })); - - // No matter what the other patterns - // are, we're definitely shadowed and will - // get a runtime exception as soon as we - // encounter the first bad pattern. - opt_erroneous = Some(Pattern::Shadowed(original_region, shadow)); - } - }; + destructs.push(Located { + region: loc_pattern.region, + value: RecordDestruct { + var: var_store.fresh(), + label: Lowercase::from(label), + symbol, + guard: Some((var_store.fresh(), can_guard)), + }, + }); } _ => panic!("invalid pattern in record"), } diff --git a/compiler/can/src/scope.rs b/compiler/can/src/scope.rs index 06c5f636f8..32d5624710 100644 --- a/compiler/can/src/scope.rs +++ b/compiler/can/src/scope.rs @@ -57,10 +57,13 @@ impl Scope { pub fn lookup(&mut self, ident: &Ident, region: Region) -> Result { match self.idents.get(ident) { Some((symbol, _)) => Ok(*symbol), - None => Err(RuntimeError::LookupNotInScope(Located { - region, - value: ident.clone().into(), - })), + None => Err(RuntimeError::LookupNotInScope( + Located { + region, + value: ident.clone().into(), + }, + self.idents.keys().map(|v| v.as_ref().into()).collect(), + )), } } @@ -107,6 +110,14 @@ impl Scope { } } + /// Ignore an identifier. + /// + /// Used for record guards like { x: Just _ } + pub fn ignore(&mut self, ident: Ident, all_ident_ids: &mut IdentIds) -> Symbol { + let ident_id = all_ident_ids.add(ident.into()); + Symbol::new(self.home, ident_id) + } + /// Import a Symbol from another module into this module's top-level scope. /// /// Returns Err if this would shadow an existing ident, including the diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index 5d5118a468..a4c50ddd1a 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -1,4 +1,5 @@ use inlinable_string::InlinableString; +use roc_collections::all::MutSet; use roc_module::ident::Ident; use roc_module::symbol::{ModuleId, Symbol}; use roc_parse::operator::BinOp; @@ -37,7 +38,7 @@ pub enum RuntimeError { // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! UnsupportedPattern(Region), UnrecognizedFunctionName(Located), - LookupNotInScope(Located), + LookupNotInScope(Located, MutSet>), ValueNotExposed { module_name: InlinableString, ident: InlinableString, diff --git a/compiler/reporting/src/report.rs b/compiler/reporting/src/report.rs index 4ea21d8b6b..848c16ffb6 100644 --- a/compiler/reporting/src/report.rs +++ b/compiler/reporting/src/report.rs @@ -1,5 +1,6 @@ use crate::report::ReportText::{BinOp, Concat, Module, Region, Value}; use bumpalo::Bump; +use roc_collections::all::MutSet; use roc_module::ident::TagName; use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_problem::can::PrecedenceProblem::BothNonAssociative; @@ -130,8 +131,16 @@ pub fn can_problem(filename: PathBuf, problem: Problem) -> Report { shadowing_report(&mut texts, original_region, shadow); } - _ => { - panic!("TODO implement run time error reporting"); + RuntimeError::LookupNotInScope(loc_name, options) => { + texts.push(not_found( + loc_name.region, + &loc_name.value, + "value", + options, + )); + } + other => { + todo!("TODO implement run time error reporting for {:?}", other); } }, }; @@ -156,6 +165,53 @@ fn shadowing_report( texts.push(plain_text("Since these variables have the same name, it's easy to use the wrong one on accident. Give one of them a new name.")); } +fn not_found( + region: roc_region::all::Region, + name: &str, + thing: &str, + options: MutSet>, +) -> ReportText { + use crate::type_error::suggest; + + let mut suggestions = suggest::sort(name, options.iter().map(|v| v.as_ref()).collect()); + suggestions.truncate(4); + + let to_details = |no_suggestion_details, yes_suggestion_details| { + if suggestions.is_empty() { + no_suggestion_details + } else { + ReportText::Stack(vec![ + yes_suggestion_details, + ReportText::Indent( + 4, + Box::new(ReportText::Stack( + suggestions + .into_iter() + .map(|v: &str| plain_text(v)) + .collect(), + )), + ), + ]) + } + }; + + let default_no = ReportText::Concat(vec![ + plain_text("Is there an "), + keyword_text("import"), + plain_text(" or "), + keyword_text("exposing"), + plain_text(" missing up-top?"), + ]); + + let default_yes = plain_text("these names seem close though:"); + + ReportText::Stack(vec![ + plain_text(&format!("I cannot find a `{}` {}", name, thing)), + ReportText::Region(region), + to_details(default_no, default_yes), + ]) +} + #[derive(Debug, Clone)] pub enum ReportText { /// A value. Render it qualified unless it was defined in the current module. diff --git a/compiler/reporting/src/type_error.rs b/compiler/reporting/src/type_error.rs index a47a1f853b..ecb6f357b6 100644 --- a/compiler/reporting/src/type_error.rs +++ b/compiler/reporting/src/type_error.rs @@ -882,6 +882,12 @@ pub mod suggest { } } + impl ToStr for &str { + fn to_str(&self) -> &str { + self + } + } + pub fn sort<'a, T>(typo: &'a str, mut options: Vec) -> Vec where T: ToStr, diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index 20c50cedd8..213db008c3 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -1500,36 +1500,98 @@ mod test_reporting { ) } - // Currently hits a bug where `x` is marked as unused - // https://github.com/rtfeldman/roc/issues/304 - // #[test] - // fn pattern_guard_mismatch() { - // report_problem_as( - // indoc!( - // r#" - // when { foo: 1 } is - // { x: True } -> 42 - // "# - // ), - // indoc!( - // r#" - // The 2nd pattern in this `when` does not match the previous ones: - // - // 3 ┆ {} -> 42 - // ┆ ^^ - // - // The 2nd pattern is trying to match record values of type: - // - // {}a - // - // But all the previous branches match: - // - // Num a - // - // "# - // ), - // ) - // } + #[test] + fn pattern_guard_mismatch() { + report_problem_as( + indoc!( + r#" + when { foo: 1 } is + { foo: True } -> 42 + "# + ), + indoc!( + r#" + The 1st pattern in this `when` is causing a mismatch: + + 2 ┆ { foo: True } -> 42 + ┆ ^^^^^^^^^^^^^ + + The first pattern is trying to match record values of type: + + { foo : [ True ]a } + + But the expression between `when` and `is` has the type: + + { foo : Num a } + + "# + ), + ) + } + + #[test] + fn pattern_guard_does_not_bind_label() { + // needs some improvement, but the principle works + report_problem_as( + indoc!( + r#" + when { foo: 1 } is + { foo: 2 } -> foo + "# + ), + indoc!( + r#" + I cannot find a `foo` value + + + 2 ┆ { foo: 2 } -> foo + ┆ ^^^ + + + these names seem close though: + Bool + Int + Num + Map + + + "# + ), + ) + } + + #[test] + fn pattern_guard_can_be_shadowed_above() { + report_problem_as( + indoc!( + r#" + foo = 3 + + when { foo: 1 } is + { foo: 2 } -> foo + "# + ), + // should give no error + "", + ) + } + + #[test] + fn pattern_guard_can_be_shadowed_below() { + report_problem_as( + indoc!( + r#" + when { foo: 1 } is + { foo: 2 } -> + foo = 3 + + foo + "# + ), + // should give no error + "", + ) + } #[test] fn pattern_or_pattern_mismatch() { diff --git a/compiler/solve/tests/test_solve.rs b/compiler/solve/tests/test_solve.rs index ca1978e59b..43b3ad9a08 100644 --- a/compiler/solve/tests/test_solve.rs +++ b/compiler/solve/tests/test_solve.rs @@ -1004,7 +1004,7 @@ mod test_solve { xEmpty = if thunk {} == 42 then { x: {} } else { x: {} } when xEmpty is - { x: {} } -> x + { x: {} } -> {} "# ), "{}", @@ -1138,7 +1138,7 @@ mod test_solve { indoc!( r#" when { x: 5 } is - { x: 4 } -> x + { x: 4 } -> 4 "# ), "Num *", diff --git a/compiler/solve/tests/test_uniq_solve.rs b/compiler/solve/tests/test_uniq_solve.rs index dd25b08985..5586ddf3cd 100644 --- a/compiler/solve/tests/test_uniq_solve.rs +++ b/compiler/solve/tests/test_uniq_solve.rs @@ -908,7 +908,7 @@ mod test_uniq_solve { xEmpty = if thunk {} == 42 then { x: {} } else { x: {} } when xEmpty is - { x: {} } -> x + { x: {} } -> {} "# ), "Attr * {}", @@ -1046,7 +1046,7 @@ mod test_uniq_solve { indoc!( r#" when { x: 5 } is - { x: 4 } -> x + { x: 4 } -> 4 "# ), "Attr * (Num (Attr * *))",