Improve error messages involving ignored variables

Fix #3987
This commit is contained in:
Ajai Nelson 2023-05-23 23:22:52 -04:00 committed by Ajai Nelson
parent 10dd57d45d
commit 2e5fef5231
No known key found for this signature in database
GPG key ID: 8B74EF43FD4CCEFF
14 changed files with 289 additions and 46 deletions

View file

@ -124,18 +124,19 @@ impl<'a> Env<'a> {
Ok(symbol)
}
None => {
let error = RuntimeError::LookupNotInScope(
Loc {
let error = RuntimeError::LookupNotInScope {
loc_name: Loc {
value: Ident::from(ident),
region,
},
scope
suggestion_options: scope
.locals
.ident_ids
.ident_strs()
.map(|(_, string)| string.into())
.collect(),
);
underscored_suggestion_region: None,
};
Err(error)
}
}

View file

@ -1019,9 +1019,18 @@ pub fn canonicalize_expr<'a>(
}
ast::Expr::Underscore(name) => {
// we parse underscores, but they are not valid expression syntax
let problem = roc_problem::can::RuntimeError::MalformedIdentifier(
(*name).into(),
roc_parse::ident::BadIdent::Underscore(region.start()),
if name.is_empty() {
roc_parse::ident::BadIdent::UnderscoreAlone(region.start())
} else {
roc_parse::ident::BadIdent::UnderscoreAtStart {
position: region.start(),
// Check if there's an ignored identifier with this name in scope (for better error messages)
declaration_region: scope.lookup_ignored_local(name),
}
},
region,
);

View file

@ -379,6 +379,12 @@ pub fn canonicalize_pattern<'a>(
Err(pattern) => pattern,
}
}
Underscore(name) => {
// An underscored identifier can't be used, but we'll still add it to the scope
// for better error messages if someone tries to use it.
scope.introduce_ignored_local(name, region);
Pattern::Underscore
}
Tag(name) => {
// Canonicalize the tag's name.
Pattern::AppliedTag {
@ -479,8 +485,6 @@ pub fn canonicalize_pattern<'a>(
ptype => unsupported_pattern(env, ptype, region),
},
Underscore(_) => Pattern::Underscore,
&NumLiteral(str) => match pattern_type {
WhenBranch => match finish_parsing_num(str) {
Err(_error) => {

View file

@ -41,6 +41,10 @@ pub struct Scope {
/// Identifiers that are in scope, and defined in the current module
pub locals: ScopedIdentIds,
/// Ignored variables (variables that start with an underscore).
/// We won't intern them because they're only used during canonicalization for error reporting.
ignored_locals: VecMap<String, Region>,
}
impl Scope {
@ -65,6 +69,7 @@ impl Scope {
abilities_store: starting_abilities_store,
shadows: VecMap::default(),
imports: default_imports,
ignored_locals: VecMap::default(),
}
}
@ -89,13 +94,17 @@ impl Scope {
match self.scope_contains_ident(ident) {
InScope(symbol, _) => Ok(symbol),
NotInScope(_) | NotPresent => {
let error = RuntimeError::LookupNotInScope(
Loc {
// identifier not found
let error = RuntimeError::LookupNotInScope {
loc_name: Loc {
region,
value: Ident::from(ident),
},
self.idents_in_scope().map(|v| v.as_ref().into()).collect(),
);
suggestion_options: self.idents_in_scope().map(|v| v.as_ref().into()).collect(),
// Check if the user just forgot to remove an underscore from an ignored identifier
underscored_suggestion_region: self.lookup_ignored_local(ident),
};
Err(error)
}
@ -418,11 +427,13 @@ impl Scope {
// - exposed_ident_count: unchanged
// - home: unchanged
let aliases_count = self.aliases.len();
let ignored_locals_count = self.ignored_locals.len();
let locals_snapshot = self.locals.in_scope.len();
let result = f(self);
self.aliases.truncate(aliases_count);
self.ignored_locals.truncate(ignored_locals_count);
// anything added in the inner scope is no longer in scope now
for i in locals_snapshot..self.locals.in_scope.len() {
@ -444,6 +455,19 @@ impl Scope {
pub fn gen_unique_symbol(&mut self) -> Symbol {
Symbol::new(self.home, self.locals.gen_unique())
}
/// Introduce a new ignored variable (variable starting with an underscore).
/// The underscore itself should not be included in `ident`.
pub fn introduce_ignored_local(&mut self, ident: &str, region: Region) {
self.ignored_locals.insert(ident.to_owned(), region);
}
/// Lookup an ignored variable (variable starting with an underscore).
/// The underscore itself should not be included in `ident`.
/// Returns the source code region of the ignored variable if it's found.
pub fn lookup_ignored_local(&self, ident: &str) -> Option<Region> {
self.ignored_locals.get(&ident.to_owned()).copied()
}
}
pub fn create_alias(