-
Notifications
You must be signed in to change notification settings - Fork 903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Salsa based red-knot prototype #11338
base: main
Are you sure you want to change the base?
Conversation
|
93dd0ac
to
bb974e2
Compare
bb974e2
to
3989cb8
Compare
CodSpeed Performance ReportMerging #11338 will not alter performanceComparing Summary
|
} | ||
|
||
#[salsa::tracked(jar=Jar)] | ||
pub struct ResolvedModule { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AlexWaygood this is where I'm currently landing on a Salsa design for a module resolver. I think it would simplify a lot for you because you no longer need to think about invalidation, Salsa will take care of that for you. The only thing necessary for this to work is that you use db.file(path).exists()
to test if a file exists.
But check out resolve_module
, it's now almost empty!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks for the ping. Yes, this indeed does make the code look a lot cleaner! It was making my head hurt a little bit to see all the cache-checking stuff right alongside the search-path semantics in resolve_module()
pub fn path_to_module(db: &dyn Db, path: &Path) -> Option<ResolvedModule> { | ||
let file = db.file(path.to_path_buf()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit weird that path_to_module
converts the path to a file
as the very first thing only so that file_to_module
then reads the path. However, for file_to_module
to be a salsa query, it can only accept an ingredient as an argument and file
is an ingredient but path
isn't.
1843679
to
06ee178
Compare
…de as derived queries.
A query result only needs to be a tracked struct if we intend to use it as a query ingredient. It's unclear to me whether this is the case for `ResolvedModule`, that's why I make it a regular struct for now. We can easily make it a tracked struct later on.
tracked-structs are only necessary when the struct should be used as an argument to a derived Salsa query. I don't expect that the lint results itself should be used as queries, therefore, normal structs do just fine.
06ee178
to
4c70337
Compare
There's one limitation with the current model where the invalidation isn't as good as it could be and it is due to the fact that we build the entire symbol table at once (we don't have to and we could refactor that later). Let's say we start with # main.py
import foo;
x = foo.x
# foo.py
x = 10
def foo():
pass And we infer the type of
When we now change the content of x = 10
def foo():
y = 10 What I expected is that the type inference for We can avoid this by also building the symbol table per scope rather than once globally. Or have a query that reduces the global symbol table to just the global symbols. I do think something like that would be nice to have more fine granular invalidations. |
…odule looking up the definition
9539ff4
to
d79ff35
Compare
This is a great write-up, thanks for taking the time! A few thoughts from the write-up, before I dive into the code:
|
The part that's unclear to me is how we would compute flags like
Yeah, but that would require that My thinking why I called the
This is what the new implementation does. But I must say, it would make me sad to see your CFG go away. I think it could be useful for other things than just typing, like an unreachable rule. |
The
This could work, too.
This is a good point. I think it's a solid enough reason for the current naming. We will need to be able to track dependencies on nonexistent modules.
"tracking narrowed local types for local symbols as we go" is the part that would specifically replace the CFG; I don't think the implementation here does that yet.
The eager version of the same logic would also have the ability to discover unreachable branches. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great PR writeup. Overall this looks good to me, though it looks like it's missing some of my recent changes to module.rs
.
I left a bunch of comments below, mostly pretty minor. I think many of them may also apply to the existing red-knot codebase -- I wouldn't yet consider myself an expert in the crate overall -- so please feel free to ignore any that you don't feel are useful. I think @carljm will probably be a much better reviewer for this in general :/
@@ -37,5 +40,6 @@ tracing-tree = { workspace = true } | |||
[dev-dependencies] | |||
tempfile = { workspace = true } | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit ;)
pub type GlobalSymbolId = GlobalId<SymbolId>; | ||
|
||
#[derive(Debug, Eq, PartialEq)] | ||
pub struct SemanticIndex { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have a docstring for this type as well
let root_scope_id = SymbolTable::root_scope_id(); | ||
let mut indexer = SemanticIndexer { | ||
db, | ||
file, | ||
symbol_table_builder: SymbolTableBuilder::new(), | ||
flow_graph_builder: FlowGraphBuilder::new(), | ||
scopes: vec![ScopeState { | ||
scope_id: root_scope_id, | ||
current_flow_node_id: FlowGraph::start(), | ||
}], | ||
current_definition: None, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should go into a new
associated method for SemanticIndexer
(or an implementation of the Default
trait)?
impl SemanticIndexer {
fn new(db: &dyn Db, file: File) -> Self {
Self {
db,
file,
symbol_table_builder: SymbolTableBuilder::new(),
flow_graph_builder: FlowGraphBuilder::new(),
scopes: vec![ScopeState {
scope_id: SymbolTable::root_scope_id(),
current_flow_node_id: FlowGraph::start(),
}],
current_definition: None,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)] | ||
pub enum FileRevision { | ||
LastModified(FileTime), | ||
#[allow(unused)] | ||
ContentHash(u128), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second variant of this enum is so that we can also detect when the "revision" of a vendored source file changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this was mainly to explore how and if we could support file revisions e.g. based on a file's hash rather than the last modified timestamp. But this isn't used right now.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] | ||
pub struct GlobalTypeId<T> | ||
where | ||
T: LocalTypeId, | ||
{ | ||
scope: TypingScope, | ||
local_id: T, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some docstrings for these *Id
types would be really helpful
let mut table = SymbolTable { | ||
scopes_by_id: IndexVec::new(), | ||
symbols_by_id: IndexVec::new(), | ||
defs: FxHashMap::default(), | ||
scopes_by_node: FxHashMap::default(), | ||
dependencies: Vec::new(), | ||
expression_scopes: IndexVec::default(), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we derived Default
on the SymbolTable
struct, this could just be
let mut table = SymbolTable { | |
scopes_by_id: IndexVec::new(), | |
symbols_by_id: IndexVec::new(), | |
defs: FxHashMap::default(), | |
scopes_by_node: FxHashMap::default(), | |
dependencies: Vec::new(), | |
expression_scopes: IndexVec::default(), | |
}; | |
let mut table = SymbolTable::default(); |
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intentionally avoided that, IIRC, because I don't want it to be possible (and especially not easy!) to create a SymbolTable without the root scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see! That wasn't obvious to me here; maybe we could think about how to make that clearer so we don't have contributors coming along and proposing the "obvious" refactor ;)
} | ||
|
||
impl<'a> ReachableDefinitionsIterator<'a> { | ||
#[allow(unused)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this shouldn't be necessary since the function's pub
?
#[allow(unused)] |
#[allow(unused)] | ||
pub fn functions(&self) -> impl Iterator<Item = (FunctionId, &StmtFunctionDef)> { | ||
self.statements | ||
.iter_enumerated() | ||
.filter_map(|(index, stmt)| Some((FunctionId(index), stmt.as_function_def_stmt()?))) | ||
} | ||
|
||
#[allow(unused)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these #[allow(unused)]
shouldn't be needed because they're pub
#[allow(unused)] | |
pub fn functions(&self) -> impl Iterator<Item = (FunctionId, &StmtFunctionDef)> { | |
self.statements | |
.iter_enumerated() | |
.filter_map(|(index, stmt)| Some((FunctionId(index), stmt.as_function_def_stmt()?))) | |
} | |
#[allow(unused)] | |
pub fn functions(&self) -> impl Iterator<Item = (FunctionId, &StmtFunctionDef)> { | |
self.statements | |
.iter_enumerated() | |
.filter_map(|(index, stmt)| Some((FunctionId(index), stmt.as_function_def_stmt()?))) | |
} | |
pub struct AstIds { | ||
expressions: IndexVec<ExpressionId, AstNodeRef<Expr>>, | ||
|
||
/// Maps expressions to their expression id. Uses `NodeKey` because it avoids cloning [`Parsed`]. | ||
expressions_map: FxHashMap<NodeKey, ExpressionId>, | ||
|
||
statements: IndexVec<StatementId, AstNodeRef<Stmt>>, | ||
|
||
statements_map: FxHashMap<NodeKey, StatementId>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider using something like https://docs.rs/bimap/latest/bimap/ here, instead of having one mapping for ID-to-expression, and another mapping for expression-to-ID (IIUC)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't, and i wasn't aware of that data structure. I prefer our implementation because we use an IndexVec
for statements and expressions
where a lookup is just an array offset whereas BiMap
would require a hash map lookup.
I wonder if what's currently called |
Thanks @AlexWaygood for the feedback. I don't plan to incorporate any of the code changes into this PR because I don't plan on merging. I'll incorporate your changes when working on the specific areas before pulling them into ruff. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked over all the code. This was a lot of work to translate all this, thanks for doing this! I don't see anything here that I think can't work in the new approach. I think overall on the semantic side this PR now has kind of a mish-mash of the old approach (per expression laziness) and the new approach (per scope typing) that is probably more complex and less efficient than we could achieve, so I expect that over the next few weeks we'll want to re-work and simplify a fair bit of it. But it makes sense to land something working with Salsa and iterate from there.
@@ -4,7 +4,7 @@ resolver = "2" | |||
|
|||
[workspace.package] | |||
edition = "2021" | |||
rust-version = "1.74" | |||
rust-version = "1.73" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we dropping our rust version in this PR? Did you add a dependency here that doesn't work with 1.74?
hashbrown = { workspace = true } | ||
indexmap = { workspace = true } | ||
notify = { workspace = true } | ||
parking_lot = { workspace = true } | ||
rayon = { workspace = true } | ||
rustc-hash = { workspace = true } | ||
salsa = { git = "https://github.com/salsa-rs/salsa.git", package = "salsa-2022", rev = "05b4e3ebdcdc47730cdd359e7e97fb2470527279" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this incorporate any of Niko's newest work on "v3" yet? Or are those changes we'll have to adapt to yet in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet, v3 is only a PR at this point. I scanned through the code and v3 is fairly close to v2022, so we're using that for now. But yes, we'll probably have to adapt some code.
|
||
impl salsa::Database for Database { | ||
fn salsa_event(&self, event: Event) { | ||
if matches!(event.kind, EventKind::WillCheckCancellation) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this event mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible to create multiple snapshots of the database that then each can run in isolation (they still share the underlying caches). This is useful when using salsa in a multithreaded context.
Now, Salsa cancels any pending snapshots (other threads) when you want to make changes to it. The way this works is that each query tests if cancellation was requested and if so, it panics with a specific error. The WillCheckCancellation
indicates that Salsa now tests cancellation.
I removed the log because it is very noisy. I think I often saw 2-3 of these logs per query. Maybe something that can be optimized later to reduce it to just one. Removing it made the log a bit more dense and easier to read thorough
#[salsa::tracked(jar=Jar)] | ||
pub fn check_syntax(db: &dyn Db, file: File) -> SyntaxCheck { | ||
// TODO I haven't looked into how many rules are pure syntax checks. | ||
// It may be necessary to at least give access to a simplified semantic model. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we would bother with a simplified semantic model (unless it's extremely simple). It seems better to just give the rules that need semantic information access to the full semantic model, and avoid inconsistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither do I but it probably also depends on what we refer to as the semantic model. Is it any information that isn't part of the AST? If so, maybe exposing the parent expression or statement is something that we can support even for syntax rules. But yeah, I don't know if it's worth it. I think this is a comment copied from the existing implementation.
let typing_scope = TypingScope::for_symbol(db, symbol); | ||
let types = infer_types(db, typing_scope); | ||
|
||
types.symbol_ty(symbol.local()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be consistent about using _ty
vs _type
in APIs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. ty
is somewhat common in the Rust ecosystem and has the advantage that it isn't a keyword (it also works for variables).
} | ||
} | ||
|
||
/// Infers the type of a location definition. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what a "location definition" is?
// The fact that the interner is local to a body means that we can't reuse the same union type | ||
// across different call sites. But that's something we aren't doing yet anyway. Our interner doesn't | ||
// deduplicate union types that are identical. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do need a place to add this deduplication (as well as the flattening/simplification that I already added in PRs since you translated this to Salsa); it's not clear to me where in this structure that should happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, agree. I think we probably want to have methods on the TypeInferenceBuilder
because we only need to e.g. track the reverse map of already created unions back to their type ids during construction but we won't need it once type inference is complete (and we won't create any new types)
} | ||
} | ||
|
||
enum DefinitionType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what a DefinitionType
is supposed to represent that is different from a Type
, or why it needs to exist at all. It seems like all it does is intern unions? (And with narrowing it will probably have to intern intersections, too.) But infer_definitions
already has a TypeInference
-- why can't it do the interning itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The enum is a lifetime hack. infer_definition
takes &self
as argument, so it can't intern a new union type.
The fact that it is a readonly reference is important in finish
where we iterate over self.symbol_table
.
for symbol in self.symbol_table.symbol_ids_for_scope(self.enclosing_scope) {
let definition_type = self.typing_context().infer_definitions(
symbol_table
.definitions(symbol)
.iter()
.map(|definition| ReachableDefinition::Definition(*definition)),
GlobalId::new(self.file, self.enclosing_scope),
);
public_symbol_types.insert(symbol, definition_type.into_type(&mut self.result));
}
Taking a &mut wouldn't compile because Rust couldn't prove that the symbol_table
doesn't get mutated (a method taking &mut self
can mutate any field). By explicitly passing &mut self.result
in into_type
Rust can prove that self.symbol_table
is never borrowed mutably
// TODO: This is going to be somewhat slow because we need to map the AST node to the expression id for | ||
// every expression in the body. That's a lot of hash map lookups. | ||
// We can't use an `IndexVec` here because a) expression ids are per module and b) the type inference | ||
// builder visits the expressions in evaluation order and not in pre-order. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The location of this comment seems odd; it's not clear what code it is referring to.
ImportDefinition { | ||
import: import_id, | ||
name: u32::try_from(i).unwrap(), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's strange to me that we build definitions in SemanticIndexer, but now we're rebuilding definitions from scratch here as well. This seems like duplication we probably don't want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is your concern just about the ImportDefinition
creation that is used as key? Because there's a difference. We associate a definition with its type.
I don't think we can avoid this much without having a way to iterate over the AST and definitions at the same time. It may be nice to have a helper that, given a StmtImport
generates the ImportDefinition
s with their metadata that could be reused across the two implementations.
This is a prototype that uses Salsa for our red-knot prototype 😆
The PR implements cross-module type inference invalidation based on Salsa. What makes this hard is that
I'll go through the important data models jar by jar.
Source
The source jar gives access to files, the text of a file, and a file's AST.
File
ruff/crates/red_knot/src/salsa_db/source.rs
Lines 31 to 44 in 946493c
The file stores the basic metadata about a file but doesn't store the file's content. This is mainly because of persistent caching. Restoring the database from disk requires that we restore all files. If the source is stored on the file, we would have to read the content of every file, and that would be very expensive (we want the source validation to happen lazily). That's why the file only stores basic metadata.
Note: We may decide long-term to have a configuration option that allows users to select if they want to use
mtime
or the file's has for change detection. In that case, I think we would have asource: Option<String>
on file so that thesource_text
query avoids re-reading the file from disk.Files are salsa inputs. Salsa doesn't know how to compute files. Instead, we need to tell salsa which files exist and when they change. That's why files are resolved using
db.file(path)
where we perform our own mapping fromPath -> File
(Salsa inputs have no identity other than their instance).SourceText
The
source_text(file: File) -> SourceText
query allows retrieving a file's source text. The source text isn't very exciting. It just stores the file's content.ruff/crates/red_knot/src/salsa_db/source.rs
Lines 163 to 167 in 946493c
Some notes about the implementation:
file.revision()
(equal to the file'smtime
) to inform Salsa that the query should rerun whenever the file is modified. It actually doesn't need the value.db.file(path)
andsource_text(db, file)
. In that case, we just assume that the file is empty. That's the best we can do without dealing with awkward results in all caller paths.parse
ruff/crates/red_knot/src/salsa_db/source.rs
Lines 175 to 183 in 946493c
The
parse
query is almost boring. It retrieves the file text and calls the parser. We opt out of Salsa'seq
optimization because the parse tree is guaranteed to change whenever the source text changes (and our AST doesn't implementEq
because of floats).Semantic
This is where it gets interesting.
AstIds
ruff/crates/red_knot/src/salsa_db/semantic/ast_ids.rs
Lines 18 to 24 in 176267f
AstIds
are a location-independent representation that allows mapping fromId -> AstNode
and fromAstNode -> Id
. The implementation tries to assign stable IDs by first giving IDs to the module-level statements and expressions, and only then traversing into the function or class level.This way, IDs of top level statements remain unchanged when only making changes to a function's body. Having stable top-level IDs is important because they are referred to from other modules.
symbols, cfg
semantic_index
ruff/crates/red_knot/src/salsa_db/semantic.rs
Lines 103 to 123 in 176267f
The
semantic_index
query computes a single file's symbol table and control flow graph. It shouldn't be used directly because thesemantic_index
changes every time the AST changes.symbol_table
ruff/crates/red_knot/src/salsa_db/semantic/symbol_table.rs
Lines 21 to 25 in 176267f
The query itself just calls into
semantic_index
. The trick here is that the symbol table itself doesn't contain any data that references the AST. Instead, all data usesAstIds
. What this query enables is that Salsa can avoid running queries that depend on thesymbol_table
if the constructed symbol table hasn't changed. For example, a comment only change doesn't invalidate the symbol table.flow_graph
ruff/crates/red_knot/src/salsa_db/semantic/flow_graph.rs
Lines 12 to 17 in 176267f
We apply the same trick for the control flow graph
Typing
typing_scopes
Typing is where the code changes the most. The existing implementation does type inference per expression. I don't think that
type_inference
per expression will be fast in Salsa because storing a query result has some overhead. Salsa is also limited to at mostu32
results per query. I think large projects could reach that limit, especially when the server runs for a long time.That's why this PR changes inference to happen per
TypingScope
instead. For now, a typing scope is either aModule
,Function
, orClass
. So this PR infers all types per module, class, or function (but the module doesn't traverse into function or class bodies).The reason why we don't perform type inference on a module scope is to get more fine-grained dependency tracking across files. The type checking of a dependency must only be rerun if the types of the scope where the symbol is defined depend on changes. If the types remain unchanged (for example because the public interface isn't changing), then type checking doesn't need to re-run.
The first step to make this possible is to create a
FunctionTypingScope
andClassTypingScope
s for everyFunction
andClass
in the file and store them in Salsa to use them as query arguments.ruff/crates/red_knot/src/salsa_db/semantic/types.rs
Lines 26 to 40 in 176267f
infer_*_body
The other important queries are
infer_module_body
,infer_function_body
, andinfer_class_body
. They perform type inference for a single module, function or class, but without traversing into nested classes or functions.ruff/crates/red_knot/src/salsa_db/semantic/types/infer.rs
Lines 39 to 64 in 176267f
Doing type-checking per block introduces some complexity. Mainly that getting the type data for a
TypeId
not just requires knowing the file from which the data needs to be read, but also from which typing scope. There's even an extra complexity. There are cases where we want to to resolve the type for atype_id
. But we may only just be building up that typing table. I solved this by introducingTypingContext
and passing that toTypeId::ty
. TheTypingContext
can have an override so that queries for a specific typing-scope are directly resolved without calling into the database.ruff/crates/red_knot/src/salsa_db/semantic/types.rs
Lines 527 to 556 in 176267f
Public API
The public API for types should be limited to:
ruff/crates/red_knot/src/salsa_db/semantic/types/infer.rs
Lines 24 to 29 in 176267f
ruff/crates/red_knot/src/salsa_db/semantic.rs
Lines 79 to 101 in 176267f
Module Resolver
The module resolver remains mostly unchanged, although I did some renaming.
Module
I think the naming could be better.
Module
is mainly aModuleName
but interned into salsa so that it can be used as a query argument.ruff/crates/red_knot/src/salsa_db/semantic/module.rs
Lines 13 to 17 in 4c70337
I didn't want to intern
ModuleName
directly because I think there are places where we want to use it without the need for having it in Salsa. But maybe that's the wrong call and we should just internModuleName
directly.resolve_module
The main query remains
resolve_module
ruff/crates/red_knot/src/salsa_db/semantic/module.rs
Lines 193 to 214 in 4c70337
What changed is that it now accepts a
Module
and returns anOption<ResolvedModule>
. Again, I'm open to suggestion for better naming. The idea is that aResolvedModule
represents to what a module name resolves. I'm consider renaming it toResolvedModulePath
because I think that's really what it is.I think the implementation became much simpler because the module resolver now uses
File
andFile::exists
internally. This has the advantage that Salsa will automatically invalidate theresolve_module
result if a relevant file gets added or removed.file_to_module
ruff/crates/red_knot/src/salsa_db/semantic/module.rs
Line 228 in 4c70337
Resolves a
file
to aOption<ResolvedModule>
if it is a module and toNone
otherwise. This is mostly unchanged.module_search_paths
andset_module_search_paths
ruff/crates/red_knot/src/salsa_db/semantic/module.rs
Lines 178 to 185 in 4c70337
These queries shouldn't exist long term but it was a "quick" way to allow setting the module search paths without supporting settings. I'll adapt this to @AlexWaygood's most recent changes by having a
set_module_resolver_settings
short term (that has fields for the different lookup paths). The long term goal is that the module resolver queries the settings and constructs the search paths from the settings (it probably should remain a query)