Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/oak_db/src/file_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ fn package_imports(file: File, db: &dyn Db, package: Package) -> Vec<ImportLayer
// and including self here would create a cycle in `resolve` for unbound
// names.
//
// TODO(scan): once `oak_scan` orders `Package.files` by the DESCRIPTION
// `Collate` field, this iteration becomes collation-ordered directly.
// Today `Package.files` is whatever order the scanner produces.
// `package.files(db)` is collation-ordered (see `Package::files`), so
// reversing it gives R's LIFO precedence: a name defined late in the
// collation shadows the same name defined earlier.
layers.extend(
package
.files(db)
Expand Down
85 changes: 64 additions & 21 deletions crates/oak_db/src/file_resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::ExportEntry;
use crate::File;
use crate::ImportLayer;
use crate::Name;
use crate::PackageVisibility;

#[salsa::tracked]
impl<'db> File {
Expand All @@ -29,12 +30,11 @@ impl<'db> File {
/// `source()`-forwarded entries. `ExportEntry::Import` is chased
/// through `exports(target)` until it lands on a `Local`. Cycles in
/// `source()` resolve to empty exports via `exports`'s `cycle_fn`.
/// 2. **`imports()` walk**: each `ImportLayer::File` sibling is checked
/// via its own exports chain only (not its full `resolve`). Sibling
/// package files would otherwise cycle through each other's
/// `imports`, and R's namespace semantics don't transitively include
/// siblings' imports anyway. Package-level layers (`From`,
/// `Package`) are deferred to PR 4.
/// 2. **`imports()` walk**: each layer is checked in priority order.
/// `File` siblings are checked via their exports chain only (not their
/// full `resolve`), to avoid the cycle that recursing would create.
/// `Package` and `From` layers call [`Package::resolve`] with
/// `Exported` visibility.
///
/// The returned `Definition` is keyed by `(file, scope, name)`, so
/// downstream queries that only depend on identity stay cached across
Expand Down Expand Up @@ -62,16 +62,36 @@ impl<'db> File {
// and the installed-package search path appear in this file's own
// `imports()` directly, as `From` / `Package` layers, so finding
// them does not require walking through siblings.
//
// TODO(sources): consume `From` and `Package` layers here. Today
// resolve only walks `ImportLayer::File`.
// Requires materializing installed-package files as `oak_db::File`
// entities first.
for layer in self.imports(db) {
if let ImportLayer::File(target) = layer {
if let Some(def) = target.resolve_export(db, name) {
return Some(def);
}
match layer {
ImportLayer::File(target) => {
if let Some(def) = target.resolve_export(db, name) {
return Some(def);
}
},
ImportLayer::Package(pkg) => {
if let Some(def) = pkg
.resolve(db, name, PackageVisibility::Exported)
.into_iter()
.next()
{
return Some(def);
}
},
ImportLayer::From(map) => {
let name_str = name.text(db).as_str();
if let Some(pkg_name) = map.get(name_str) {
if let Some(pkg) = db.package_by_name(pkg_name) {
if let Some(def) = pkg
.resolve(db, name, PackageVisibility::Exported)
.into_iter()
.next()
{
Comment on lines +74 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These use next() and only take the first hit, should this function return Vec<Definition>?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if so, should there be corresponding tests somewhere that ensure that multiple definitions work?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, resolved by #1259

return Some(def);
}
}
}
},
}
}

Expand Down Expand Up @@ -129,12 +149,31 @@ impl<'db> File {

// Top level: collation predecessors / other visible files (exports-only
// chase, same as `resolve`'s imports walk). Avoids the sibling cycle and
// matches R's namespace semantics. TODO: Package-level layers.
// matches R's namespace semantics.
for layer in self.imports_at(db, offset) {
if let ImportLayer::File(target) = layer {
if let Some(def) = target.resolve_export(db, name) {
return vec![def];
}
match layer {
ImportLayer::File(target) => {
if let Some(def) = target.resolve_export(db, name) {
return vec![def];
}
},
ImportLayer::Package(pkg) => {
let defs = pkg.resolve(db, name, PackageVisibility::Exported);
if !defs.is_empty() {
return defs;
}
},
ImportLayer::From(map) => {
let name_str = name.text(db).as_str();
if let Some(pkg_name) = map.get(name_str) {
if let Some(pkg) = db.package_by_name(pkg_name) {
let defs = pkg.resolve(db, name, PackageVisibility::Exported);
if !defs.is_empty() {
return defs;
}
}
}
},
}
}

Expand Down Expand Up @@ -164,7 +203,11 @@ impl<'db> File {
/// `Import` entries through target exports until a `Local` is found. Cycles
/// resolve to `None` via `exports`'s `cycle_fn`.
#[salsa::tracked]
fn resolve_export(self, db: &'db dyn Db, name: Name<'db>) -> Option<Definition<'db>> {
pub(crate) fn resolve_export(
self,
db: &'db dyn Db,
name: Name<'db>,
) -> Option<Definition<'db>> {
let mut current_file = self;
let mut current_name: Rc<str> = Rc::from(name.text(db).as_str());

Expand Down
9 changes: 8 additions & 1 deletion crates/oak_db/src/inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,14 @@ pub struct Package {
pub version: Option<String>,
#[returns(ref)]
pub namespace: Namespace,
/// R source files belonging to this package (the `R/*.R` files).
/// R source files belonging to this package (the `R/*.R` files), in
/// R's load order. When DESCRIPTION's `Collate:` directive is
/// present, this is exactly the files it lists, in that order;
/// files in `R/` not listed are excluded (matching R's loader,
/// Writing R Extensions §1.1.1). When `Collate:` is absent, files
/// are in case-insensitive alphabetical order. TODO(diagnostics):
/// Lint files missing from collation.
///
/// Per-package granularity: adding or removing a file in one
/// package doesn't invalidate tracked queries reading another
/// package's files.
Expand Down
2 changes: 2 additions & 0 deletions crates/oak_db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod identifier;
mod imports;
mod inputs;
mod name;
mod package_resolve;
mod parse;
mod storage;

Expand All @@ -34,4 +35,5 @@ pub use inputs::RootKind;
pub use inputs::StaleRoot;
pub use inputs::WorkspaceRoots;
pub use name::Name;
pub use package_resolve::PackageVisibility;
pub use storage::OakDatabase;
92 changes: 92 additions & 0 deletions crates/oak_db/src/package_resolve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::Db;
use crate::Definition;
use crate::Name;
use crate::Package;

/// Visibility filter for [`Package::resolve`].
///
/// Mirrors R's `::` vs `:::` distinction. `Exported` requires `name` to
/// appear in the package's NAMESPACE `export()` directives. `Internal`
/// returns any top-level binding the package's files define, exported
/// or not.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PackageVisibility {
Exported,
Internal,
}

#[salsa::tracked]
impl<'db> Package {
/// Resolve `name` against this package's top-level bindings.
///
/// - `Exported` (R's `pkg::name`) gates on the package's NAMESPACE `export()`
/// directives: an internal binding is invisible even if it exists.
/// - `Internal` (R's `pkg:::name`) returns any top-level binding regardless
/// of NAMESPACE.
///
/// Iterates [`Package::files`] and aggregates each file's
/// [`File::resolve_export`] for `name`.
///
/// Returns a `Vec` because a package's namespace can carry more than one
/// binding per name. A common pattern is a top-level stub overridden from
/// an `.onLoad` hook via `<<-`:
Comment on lines +30 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But overwriting via <<- would never result in a 2nd top level binding, so how can it come back as a candidate?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, or is it that <<- itself is special in the semantic index and registers foo as a top level binding even though its found in the onLoad scope?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right <<- is additive, not overwriting, like when two arms of an if/else condition define the same name.

///
/// ```r
/// # R/foo.R
/// foo <- function() stop("not loaded yet")
///
/// # R/zzz.R
/// .onLoad <- function(libname, pkgname) {
/// foo <<- function() "real implementation"
/// }
/// ```
///
/// Both bindings live in the package namespace; both come back as
/// candidates. Conditional fan-out within a single file (e.g.
/// `if cond x <- 1 else x <- 2`) surfaces here the same way.
///
/// Returns an empty Vec when the name isn't bound anywhere in the package,
/// or when `Exported` is requested for a name absent from
/// `namespace.exports`.
#[salsa::tracked]
pub fn resolve(
self,
db: &'db dyn Db,
name: Name<'db>,
visibility: PackageVisibility,
) -> Vec<Definition<'db>> {
if visibility == PackageVisibility::Exported &&
!self
.namespace(db)
.exports
.contains_str(name.text(db).as_str())
{
return Vec::new();
}

let mut results = Vec::new();
for &file in self.files(db) {
results.extend(file.resolve_export(db, name));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do re-exports work? i.e. does export(tibble) work when you just have tibble::tibble in the R file?
https://github.com/tidyverse/dplyr/blob/d5e94e7fa8fd4a5f79c1a707d1842216bb4c691f/R/reexport-tibble.R#L23

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They did not! Now done, good catch

}

// Re-exports. A name brought in by `importFrom()` and surfaced again by
// `export()` has no binding in this package's own files, so the loop
// above returns empty. Follow the import to the source package and
// resolve the name among its exports instead.
if results.is_empty() {
let name_str = name.text(db).as_str();
if let Some(import) = self
.namespace(db)
.imports
.iter()
.find(|import| import.name == name_str)
{
if let Some(source) = db.package_by_name(&import.package) {
results = source.resolve(db, name, PackageVisibility::Exported);
}
}
}

results
}
}
1 change: 1 addition & 0 deletions crates/oak_db/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ mod file_resolve_at;
mod file_root;
mod identifier;
mod inputs;
mod package_resolve;
mod resolver;
mod test_db;
Loading
Loading