Load and can imports inside defs

After parsing a module, we now recursively traverse the tree to find
all imports inside Defs, not just the top-level ones.

Previously, imported modules were available in the entire file,
but that's no longer the case. Therefore, Scope now keeps track of
imported modules and Env::qualified_lookup checks whether a module
is available in the provided scope.

Note: Unused import warnings are still global and need to be updated.
This commit is contained in:
Agus Zubiaga 2024-01-06 18:34:04 -03:00
parent 710d62f754
commit c617963b22
No known key found for this signature in database
10 changed files with 434 additions and 84 deletions

View file

@ -28,6 +28,7 @@ use roc_collections::{ImSet, MutMap, SendMap};
use roc_error_macros::internal_error;
use roc_module::ident::Ident;
use roc_module::ident::Lowercase;
use roc_module::ident::ModuleName;
use roc_module::symbol::IdentId;
use roc_module::symbol::ModuleId;
use roc_module::symbol::Symbol;
@ -2887,23 +2888,32 @@ fn to_pending_value_def<'a>(
}),
ModuleImport(module_import) => {
let module_name = ModuleName::from(module_import.name.value.name);
let module_id = env
.module_ids
.get_id(&module_name)
.expect("Module id should have been added in load");
scope.import_module(module_id);
match module_import.exposed {
None => {}
Some(exposed) if exposed.item.is_empty() => {}
Some(exposed) => {
for loc_name in exposed.item.items {
Some(exposed_kw) => {
let exposed_ids = env
.dep_idents
.get(&module_id)
.expect("Module id should have been added in load");
for loc_name in exposed_kw.item.items {
let exposed_name = loc_name.value.item();
let name = exposed_name.as_str();
let ident = name.into();
let ident = Ident::from(name);
match env.qualified_lookup(
scope,
module_import.name.value.name,
name,
loc_name.region,
) {
Ok(imported_symbol) => {
match scope.import(ident, imported_symbol, loc_name.region) {
match exposed_ids.get_id(name) {
Some(ident_id) => {
let symbol = Symbol::new(module_id, ident_id);
match scope.import_symbol(ident, symbol, loc_name.region) {
Ok(()) => {}
Err((_shadowed_symbol, _region)) => {
internal_error!(
@ -2912,8 +2922,21 @@ fn to_pending_value_def<'a>(
}
}
}
Err(problem) => {
env.problem(Problem::RuntimeError(problem.clone()));
None => {
let exposed_values = exposed_ids
.ident_strs()
.filter(|(_, ident)| {
ident.starts_with(|c: char| c.is_lowercase())
})
.map(|(_, ident)| Lowercase::from(ident))
.collect();
env.problem(Problem::RuntimeError(RuntimeError::ValueNotExposed {
module_name: module_name.clone(),
ident,
region: loc_name.region,
exposed_values,
}))
}
}
}

View file

@ -141,37 +141,39 @@ impl<'a> Env<'a> {
}
} else {
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);
Some(exposed_ids) if scope.has_imported_module(&module_id) => {
match exposed_ids.get_id(ident) {
Some(ident_id) => {
let symbol = Symbol::new(module_id, ident_id);
if is_type_name {
self.qualified_type_lookups.insert(symbol);
} else {
self.qualified_value_lookups.insert(symbol);
if is_type_name {
self.qualified_type_lookups.insert(symbol);
} else {
self.qualified_value_lookups.insert(symbol);
}
Ok(symbol)
}
None => {
let exposed_values = exposed_ids
.ident_strs()
.filter(|(_, ident)| ident.starts_with(|c: char| c.is_lowercase()))
.map(|(_, ident)| Lowercase::from(ident))
.collect();
Err(RuntimeError::ValueNotExposed {
module_name: self
.module_ids
.get_name(module_id)
.expect("Module ID known, but not in the module IDs somehow")
.clone(),
ident: Ident::from(ident),
region,
exposed_values,
})
}
Ok(symbol)
}
None => {
let exposed_values = exposed_ids
.ident_strs()
.filter(|(_, ident)| ident.starts_with(|c: char| c.is_lowercase()))
.map(|(_, ident)| Lowercase::from(ident))
.collect();
Err(RuntimeError::ValueNotExposed {
module_name: self
.module_ids
.get_name(module_id)
.expect("Module ID known, but not in the module IDs somehow")
.clone(),
ident: Ident::from(ident),
region,
exposed_values,
})
}
},
None => Err(RuntimeError::ModuleNotImported {
}
_ => Err(RuntimeError::ModuleNotImported {
module_name: self
.module_ids
.get_name(module_id)

View file

@ -326,7 +326,7 @@ pub fn canonicalize_module_defs<'a>(
let first_char = ident.as_inline_str().as_str().chars().next().unwrap();
if first_char.is_lowercase() {
match scope.import(ident, symbol, region) {
match scope.import_symbol(ident, symbol, region) {
Ok(()) => {
// Add an entry to exposed_imports using the current module's name
// as the key; e.g. if this is the Foo module and we have
@ -349,7 +349,7 @@ pub fn canonicalize_module_defs<'a>(
// but now we know this symbol by a different identifier, so we still need to add it to
// the scope
match scope.import(ident, symbol, region) {
match scope.import_symbol(ident, symbol, region) {
Ok(()) => {
// here we do nothing special
}

View file

@ -29,8 +29,11 @@ pub struct Scope {
/// The first `exposed_ident_count` identifiers are exposed
exposed_ident_count: usize,
/// Identifiers that are imported (and introduced in the header)
imports: Vec<(Ident, Symbol, Region)>,
/// Modules that are imported
imported_modules: Vec<ModuleId>,
/// Identifiers that are imported
imported_symbols: Vec<(Ident, Symbol, Region)>,
/// Shadows of an ability member, for example a local specialization of `eq` for the ability
/// member `Eq implements eq : a, a -> Bool where a implements Eq` gets a shadow symbol it can use for its
@ -68,7 +71,8 @@ impl Scope {
aliases: VecMap::default(),
abilities_store: starting_abilities_store,
shadows: VecMap::default(),
imports: default_imports,
imported_modules: Vec::default(),
imported_symbols: default_imports,
ignored_locals: VecMap::default(),
}
}
@ -82,9 +86,9 @@ impl Scope {
}
pub fn add_docs_imports(&mut self) {
self.imports
self.imported_symbols
.push(("Dict".into(), Symbol::DICT_DICT, Region::zero()));
self.imports
self.imported_symbols
.push(("Set".into(), Symbol::SET_SET, Region::zero()));
}
@ -113,7 +117,7 @@ impl Scope {
fn idents_in_scope(&self) -> impl Iterator<Item = Ident> + '_ {
let it1 = self.locals.idents_in_scope();
let it2 = self.imports.iter().map(|t| t.0.clone());
let it2 = self.imported_symbols.iter().map(|t| t.0.clone());
it2.chain(it1)
}
@ -139,7 +143,7 @@ impl Scope {
},
None => {
// opaque types can only be wrapped/unwrapped in the scope they are defined in (and below)
let error = if let Some((_, decl_region)) = self.has_imported(opaque_str) {
let error = if let Some((_, decl_region)) = self.has_imported_symbol(opaque_str) {
// specific error for when the opaque is imported, which definitely does not work
RuntimeError::OpaqueOutsideScope {
opaque,
@ -202,8 +206,12 @@ impl Scope {
}
}
fn has_imported(&self, ident: &str) -> Option<(Symbol, Region)> {
for (import, shadow, original_region) in self.imports.iter() {
pub fn has_imported_module(&self, module_id: &ModuleId) -> bool {
module_id.is_builtin() || self.imported_modules.contains(module_id)
}
fn has_imported_symbol(&self, ident: &str) -> Option<(Symbol, Region)> {
for (import, shadow, original_region) in self.imported_symbols.iter() {
if ident == import.as_str() {
return Some((*shadow, *original_region));
}
@ -215,7 +223,7 @@ impl Scope {
/// Is an identifier in scope, either in the locals or imports
fn scope_contains_ident(&self, ident: &str) -> ContainsIdent {
// exposed imports are likely to be small
match self.has_imported(ident) {
match self.has_imported_symbol(ident) {
Some((symbol, region)) => ContainsIdent::InScope(symbol, region),
None => self.locals.contains_ident(ident),
}
@ -379,21 +387,25 @@ impl Scope {
///
/// Returns Err if this would shadow an existing ident, including the
/// Symbol and Region of the ident we already had in scope under that name.
pub fn import(
pub fn import_symbol(
&mut self,
ident: Ident,
symbol: Symbol,
region: Region,
) -> Result<(), (Symbol, Region)> {
if let Some((s, r)) = self.has_imported(ident.as_str()) {
if let Some((s, r)) = self.has_imported_symbol(ident.as_str()) {
return Err((s, r));
}
self.imports.push((ident, symbol, region));
self.imported_symbols.push((ident, symbol, region));
Ok(())
}
pub fn import_module(&mut self, module_id: ModuleId) {
self.imported_modules.push(module_id);
}
pub fn add_alias(
&mut self,
name: Symbol,
@ -423,17 +435,22 @@ impl Scope {
//
// - abilities_store: ability definitions not allowed in inner scopes
// - locals: everything introduced in the inner scope is marked as not in scope in the rollback
// - imports: everything that was imported in the inner scope is removed in the rollback
// - aliases: stored in a VecMap, we just discard anything added in an inner 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 imported_modules_snapshot = self.imported_modules.len();
let imported_symbols_snapshot = self.imported_symbols.len();
let result = f(self);
self.aliases.truncate(aliases_count);
self.ignored_locals.truncate(ignored_locals_count);
self.imported_modules.truncate(imported_modules_snapshot);
self.imported_symbols.truncate(imported_symbols_snapshot);
// anything added in the inner scope is no longer in scope now
for i in locals_snapshot..self.locals.in_scope.len() {
@ -820,7 +837,7 @@ mod test {
assert!(scope.lookup(&ident, region).is_err());
assert!(scope.import(ident.clone(), symbol, region).is_ok());
assert!(scope.import_symbol(ident.clone(), symbol, region).is_ok());
assert!(scope.lookup(&ident, region).is_ok());
@ -842,7 +859,7 @@ mod test {
let region1 = Region::from_pos(Position { offset: 10 });
let region2 = Region::from_pos(Position { offset: 20 });
scope.import(ident.clone(), symbol, region1).unwrap();
scope.import_symbol(ident.clone(), symbol, region1).unwrap();
let (original, _ident, shadow_symbol) =
scope.introduce(ident.clone(), region2).unwrap_err();