Allow to specify a dependency on a query group without making it a super trait.
Currently, there's only one way to express that queries from group A
can use
another group B
: namely, B
can be a super-trait of A
:
#[salsa::query_group(AStorage)]
trait A: B {
}
This approach works and allows one to express complex dependencies. However,
this approach falls down when one wants to make a dependency a private
implementation detail: Clients with db: &impl A
can freely call B
methods on
the db
.
This is a bad situation from software engineering point of view: if everything is accessible, it's hard to make distinction between public API and private implementation details. In the context of salsa the situation is even worse, because it breaks "firewall" pattern. It's customary to wrap low-level frequently-changing or volatile queries into higher-level queries which produce stable results and contain invalidation. In the current salsa, however, it's very easy to accidentally call a low-level volatile query instead of a wrapper, introducing and undesired dependency.
To specify query dependencies, a requires
attribute should be used:
#[salsa::query_group(SymbolsDatabaseStorage)]
#[salsa::requires(SyntaxDatabase)]
#[salsa::requires(EnvDatabase)]
pub trait SymbolsDatabase {
fn get_symbol_by_name(&self, name: String) -> Symbol;
}
The argument of requires
is a path to a trait. The traits from all requires
attributes are available when implementing the query:
fn get_symbol_by_name(
db: &(impl SymbolsDatabase + SyntaxDatabase + EnvDatabase),
name: String,
) -> Symbol {
// ...
}
However, these traits are not available without explicit bounds:
fn fuzzy_find_symbol(db: &impl SymbolsDatabase, name: String) {
// Can't accidentally call methods of the `SyntaxDatabase`
}
Note that, while the RFC does not propose to add per-query dependencies, query
implementation can voluntarily specify only a subset of traits from requires
attribute:
fn get_symbol_by_name(
// Purposefully don't depend on EnvDatabase
db: &(impl SymbolsDatabase + SyntaxDatabase),
name: String,
) -> Symbol {
// ...
}
The implementation is straightforward and consists of adding traits from
requires
attributes to various where
bounds. For example, we would generate
the following blanket for above example:
impl<T> SymbolsDatabase for T
where
T: SyntaxDatabase + EnvDatabase,
T: salsa::plumbing::HasQueryGroup<SymbolsDatabaseStorage>
{
...
}
The semantics of requires
closely resembles where
, so we could imagine a
syntax based on magical where clauses:
#[salsa::query_group(SymbolsDatabaseStorage)]
pub trait SymbolsDatabase
where ???: SyntaxDatabase + EnvDatabase
{
fn get_symbol_by_name(&self, name: String) -> Symbol;
}
However, it's not obvious what should stand for ???
. Self
won't be ideal,
because supertraits are a sugar for bounds on Self
, and we deliberately want
different semantics. Perhaps picking a magical identifier like DB
would work
though?
One potential future development here is per-query-function bounds, but they can already be simulated by voluntarily requiring less bounds in the implementation function.
Another direction for future work is privacy: because traits from requires
clause are not a part of public interface, in theory it should be possible to
restrict their visibility. In practice, this still hits public-in-private lint,
at least with a trivial implementation.