diff --git a/ast/src/lang/env.rs b/ast/src/lang/env.rs index 03d93f561e..04596cd84c 100644 --- a/ast/src/lang/env.rs +++ b/ast/src/lang/env.rs @@ -1,7 +1,7 @@ use crate::mem_pool::pool::{NodeId, Pool}; use bumpalo::{collections::Vec as BumpVec, Bump}; use roc_collections::all::{MutMap, MutSet}; -use roc_module::ident::{Ident, ModuleName}; +use roc_module::ident::{Ident, Lowercase, ModuleName}; use roc_module::symbol::{IdentIds, ModuleId, ModuleIds, Symbol}; use roc_problem::can::{Problem, RuntimeError}; use roc_region::all::{Located, Region}; @@ -134,23 +134,37 @@ impl<'a> Env<'a> { )), } } else { - match self - .dep_idents - .get(&module_id) - .and_then(|exposed_ids| exposed_ids.get_id(&ident)) - { - Some(ident_id) => { - let symbol = Symbol::new(module_id, *ident_id); + match self.dep_idents.get(&module_id) { + Some(exposed_ids) => match exposed_ids.get_id(&ident) { + Some(ident_id) => { + let symbol = Symbol::new(module_id, *ident_id); - self.qualified_lookups.insert(symbol); + self.qualified_lookups.insert(symbol); - Ok(symbol) + Ok(symbol) + } + None => { + let exposed_values = exposed_ids + .idents() + .filter(|(_, ident)| { + ident.as_ref().starts_with(|c: char| c.is_lowercase()) + }) + .map(|(_, ident)| Lowercase::from(ident.as_ref())) + .collect(); + Err(RuntimeError::ValueNotExposed { + module_name, + ident, + region, + exposed_values, + }) + } + }, + None => { + panic!( + "Module {} exists, but is not recorded in dep_idents", + module_name + ) } - None => Err(RuntimeError::ValueNotExposed { - module_name, - ident, - region, - }), } } } diff --git a/compiler/can/src/env.rs b/compiler/can/src/env.rs index 3054193b65..c91fa67bae 100644 --- a/compiler/can/src/env.rs +++ b/compiler/can/src/env.rs @@ -1,6 +1,6 @@ use crate::procedure::References; use roc_collections::all::{MutMap, MutSet}; -use roc_module::ident::{Ident, ModuleName}; +use roc_module::ident::{Ident, Lowercase, ModuleName}; use roc_module::symbol::{IdentIds, ModuleId, ModuleIds, Symbol}; use roc_problem::can::{Problem, RuntimeError}; use roc_region::all::{Located, Region}; @@ -99,23 +99,37 @@ impl<'a> Env<'a> { )), } } else { - match self - .dep_idents - .get(&module_id) - .and_then(|exposed_ids| exposed_ids.get_id(&ident)) - { - Some(ident_id) => { - let symbol = Symbol::new(module_id, *ident_id); + match self.dep_idents.get(&module_id) { + Some(exposed_ids) => match exposed_ids.get_id(&ident) { + Some(ident_id) => { + let symbol = Symbol::new(module_id, *ident_id); - self.qualified_lookups.insert(symbol); + self.qualified_lookups.insert(symbol); - Ok(symbol) + Ok(symbol) + } + None => { + let exposed_values = exposed_ids + .idents() + .filter(|(_, ident)| { + ident.as_ref().starts_with(|c: char| c.is_lowercase()) + }) + .map(|(_, ident)| Lowercase::from(ident.as_ref())) + .collect(); + Err(RuntimeError::ValueNotExposed { + module_name, + ident, + region, + exposed_values, + }) + } + }, + None => { + panic!( + "Module {} exists, but is not recorded in dep_idents", + module_name + ) } - None => Err(RuntimeError::ValueNotExposed { - module_name, - ident, - region, - }), } } } diff --git a/compiler/module/src/symbol.rs b/compiler/module/src/symbol.rs index d4446ce2cf..8595a3a456 100644 --- a/compiler/module/src/symbol.rs +++ b/compiler/module/src/symbol.rs @@ -992,12 +992,16 @@ define_builtins! { } 2 BOOL: "Bool" => { 0 BOOL_BOOL: "Bool" imported // the Bool.Bool type alias - 1 BOOL_AND: "and" - 2 BOOL_OR: "or" - 3 BOOL_NOT: "not" - 4 BOOL_XOR: "xor" - 5 BOOL_EQ: "isEq" - 6 BOOL_NEQ: "isNotEq" + 1 BOOL_FALSE: "False" imported // Bool.Bool = [ False, True ] + // NB: not strictly needed; used for finding global tag names in error suggestions + 2 BOOL_TRUE: "True" imported // Bool.Bool = [ False, True ] + // NB: not strictly needed; used for finding global tag names in error suggestions + 3 BOOL_AND: "and" + 4 BOOL_OR: "or" + 5 BOOL_NOT: "not" + 6 BOOL_XOR: "xor" + 7 BOOL_EQ: "isEq" + 8 BOOL_NEQ: "isNotEq" } 3 STR: "Str" => { 0 STR_STR: "Str" imported // the Str.Str type alias @@ -1082,12 +1086,16 @@ define_builtins! { } 5 RESULT: "Result" => { 0 RESULT_RESULT: "Result" imported // the Result.Result type alias - 1 RESULT_MAP: "map" - 2 RESULT_MAP_ERR: "mapErr" - 3 RESULT_WITH_DEFAULT: "withDefault" - 4 RESULT_AFTER: "after" - 5 RESULT_IS_OK: "isOk" - 6 RESULT_IS_ERR: "isErr" + 1 RESULT_OK: "Ok" imported // Result.Result a e = [ Ok a, Err e ] + // NB: not strictly needed; used for finding global tag names in error suggestions + 2 RESULT_ERR: "Err" imported // Result.Result a e = [ Ok a, Err e ] + // NB: not strictly needed; used for finding global tag names in error suggestions + 3 RESULT_MAP: "map" + 4 RESULT_MAP_ERR: "mapErr" + 5 RESULT_WITH_DEFAULT: "withDefault" + 6 RESULT_AFTER: "after" + 7 RESULT_IS_OK: "isOk" + 8 RESULT_IS_ERR: "isErr" } 6 DICT: "Dict" => { 0 DICT_DICT: "Dict" imported // the Dict.Dict type alias diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index c5bfa42f53..d704d02dc1 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -139,6 +139,7 @@ pub enum RuntimeError { module_name: ModuleName, ident: Ident, region: Region, + exposed_values: Vec, }, ModuleNotImported { module_name: ModuleName, diff --git a/reporting/src/error/canonicalize.rs b/reporting/src/error/canonicalize.rs index 13d3406593..a3e5d7186a 100644 --- a/reporting/src/error/canonicalize.rs +++ b/reporting/src/error/canonicalize.rs @@ -6,6 +6,7 @@ use roc_problem::can::{BadPattern, FloatErrorKind, IntErrorKind, Problem, Runtim use roc_region::all::{Located, Region}; use std::path::PathBuf; +use crate::error::r#type::suggest; use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder, Severity}; use ven_pretty::DocAllocator; @@ -874,16 +875,36 @@ fn pretty_runtime_error<'b>( module_name, ident, region, + exposed_values, } => { + let mut suggestions = suggest::sort(ident.as_ref(), exposed_values); + suggestions.truncate(4); + + let did_you_mean = if suggestions.is_empty() { + alloc.concat(vec![ + alloc.reflow("In fact, it looks like "), + alloc.module_name(module_name.clone()), + alloc.reflow(" doesn't expose any values!"), + ]) + } else { + let qualified_suggestions = suggestions + .into_iter() + .map(|v| alloc.string(module_name.to_string() + "." + v.as_str())); + alloc.stack(vec![ + alloc.reflow("Did you mean one of these?"), + alloc.vcat(qualified_suggestions).indent(4), + ]) + }; doc = alloc.stack(vec![ alloc.concat(vec![ alloc.reflow("The "), alloc.module_name(module_name), - alloc.reflow(" module does not expose a "), + alloc.reflow(" module does not expose `"), alloc.string(ident.to_string()), - alloc.reflow(" value:"), + alloc.reflow("`:"), ]), alloc.region(region), + did_you_mean, ]); title = VALUE_NOT_EXPOSED; @@ -1176,8 +1197,6 @@ fn not_found<'b>( thing: &'b str, options: MutSet>, ) -> RocDocBuilder<'b> { - use crate::error::r#type::suggest; - let mut suggestions = suggest::sort( name.as_inline_str().as_str(), options.iter().map(|v| v.as_ref()).collect(), @@ -1225,8 +1244,6 @@ fn module_not_found<'b>( name: &ModuleName, options: MutSet>, ) -> RocDocBuilder<'b> { - use crate::error::r#type::suggest; - let mut suggestions = suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect()); suggestions.truncate(4); diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs index 5210c72396..354a5e5bb2 100644 --- a/reporting/tests/test_reporting.rs +++ b/reporting/tests/test_reporting.rs @@ -298,17 +298,24 @@ mod test_reporting { report_problem_as( indoc!( r#" - List.foobar 1 2 + List.isempty 1 2 "# ), indoc!( r#" ── NOT EXPOSED ───────────────────────────────────────────────────────────────── - The List module does not expose a foobar value: + The List module does not expose `isempty`: - 1│ List.foobar 1 2 - ^^^^^^^^^^^ + 1│ List.isempty 1 2 + ^^^^^^^^^^^^ + + Did you mean one of these? + + List.isEmpty + List.set + List.get + List.keepIf "# ), ) @@ -547,7 +554,35 @@ mod test_reporting { baz Nat Str - U8 + Err + "# + ), + ) + } + + #[test] + fn lowercase_primitive_tag_bool() { + report_problem_as( + indoc!( + r#" + if true then 1 else 2 + "# + ), + indoc!( + r#" + ── UNRECOGNIZED NAME ─────────────────────────────────────────────────────────── + + I cannot find a `true` value + + 1│ if true then 1 else 2 + ^^^^ + + Did you mean one of these? + + True + Str + Num + Err "# ), ) @@ -1950,10 +1985,10 @@ mod test_reporting { Did you mean one of these? + Ok U8 f I8 - F64 "# ), ) @@ -5596,10 +5631,17 @@ mod test_reporting { r#" ── NOT EXPOSED ───────────────────────────────────────────────────────────────── - The Num module does not expose a if value: + The Num module does not expose `if`: 1│ Num.if ^^^^^^ + + Did you mean one of these? + + Num.sin + Num.div + Num.abs + Num.neg "# ), ) @@ -5802,8 +5844,8 @@ mod test_reporting { Nat Str + Err U8 - F64 "# ), )