Transition to salsa coarse-grained tracked structs (#15763)

## Summary

Transition to using coarse-grained tracked structs (depends on
https://github.com/salsa-rs/salsa/pull/657). For now, this PR doesn't
add any `#[tracked]` fields, meaning that any changes cause the entire
struct to be invalidated. It also changes `AstNodeRef` to be
compared/hashed by pointer address, instead of performing a deep AST
comparison.

## Test Plan

This yields a 10-15% improvement on my machine (though weirdly some runs
were 5-10% without being flagged as inconsistent by criterion, is there
some non-determinism involved?). It's possible that some of this is
unrelated, I'll try applying the patch to the current salsa version to
make sure.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Ibraheem Ahmed 2025-02-11 05:38:50 -05:00 committed by GitHub
parent 7fbd89cb39
commit 69d86d1d69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 137 additions and 223 deletions

17
Cargo.lock generated
View file

@ -29,6 +29,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -1096,6 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [ dependencies = [
"ahash", "ahash",
"allocator-api2",
] ]
[[package]] [[package]]
@ -2870,6 +2877,7 @@ name = "ruff_index"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"ruff_macros", "ruff_macros",
"salsa",
"static_assertions", "static_assertions",
] ]
@ -2980,6 +2988,7 @@ dependencies = [
"ruff_source_file", "ruff_source_file",
"ruff_text_size", "ruff_text_size",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"salsa",
"schemars", "schemars",
"serde", "serde",
] ]
@ -3304,12 +3313,14 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]] [[package]]
name = "salsa" name = "salsa"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0" source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
dependencies = [ dependencies = [
"append-only-vec", "append-only-vec",
"arc-swap", "arc-swap",
"compact_str",
"crossbeam", "crossbeam",
"dashmap 6.1.0", "dashmap 6.1.0",
"hashbrown 0.14.5",
"hashlink", "hashlink",
"indexmap", "indexmap",
"parking_lot", "parking_lot",
@ -3324,12 +3335,12 @@ dependencies = [
[[package]] [[package]]
name = "salsa-macro-rules" name = "salsa-macro-rules"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0" source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
[[package]] [[package]]
name = "salsa-macros" name = "salsa-macros"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0" source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",

View file

@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" } regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" } rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" } salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
schemars = { version = "0.8.16" } schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" } seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }

View file

@ -12,9 +12,9 @@ license = { workspace = true }
[dependencies] [dependencies]
ruff_db = { workspace = true } ruff_db = { workspace = true }
ruff_index = { workspace = true } ruff_index = { workspace = true, features = ["salsa"] }
ruff_macros = { workspace = true } ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true } ruff_python_ast = { workspace = true, features = ["salsa"] }
ruff_python_parser = { workspace = true } ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true } ruff_python_stdlib = { workspace = true }
ruff_source_file = { workspace = true } ruff_source_file = { workspace = true }
@ -31,7 +31,7 @@ drop_bomb = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
ordermap = { workspace = true } ordermap = { workspace = true }
salsa = { workspace = true } salsa = { workspace = true, features = ["compact_str"] }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }

View file

@ -12,7 +12,29 @@ use ruff_db::parsed::ParsedModule;
/// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released. /// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released.
/// ///
/// ## Equality /// ## Equality
/// Two `AstNodeRef` are considered equal if their wrapped nodes are equal. /// Two `AstNodeRef` are considered equal if their pointer addresses are equal.
///
/// ## Usage in salsa tracked structs
/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
/// everytime the AST changes, which in turn, invalidates the result of any query
/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
///
/// For example, marking the [`AstNodeRef`] as tracked on `Expression`
/// has the effect that salsa will consider the expression as "unchanged" for as long as it:
///
/// * belongs to the same file
/// * belongs to the same scope
/// * has the same kind
/// * was created in the same order
///
/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
/// us some form of scope-stable identity for expressions. Only queries accessing the node field
/// run on every AST change. All other queries only run when the expression's identity changes.
///
/// The one exception to this is if it is known that all queries tacking the tracked struct
/// as argument or returning it as part of their result are known to access the node field.
/// Marking the field tracked is then unnecessary.
#[derive(Clone)] #[derive(Clone)]
pub struct AstNodeRef<T> { pub struct AstNodeRef<T> {
/// Owned reference to the node's [`ParsedModule`]. /// Owned reference to the node's [`ParsedModule`].
@ -67,23 +89,17 @@ where
} }
} }
impl<T> PartialEq for AstNodeRef<T> impl<T> PartialEq for AstNodeRef<T> {
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.node().eq(other.node()) self.node.eq(&other.node)
} }
} }
impl<T> Eq for AstNodeRef<T> where T: Eq {} impl<T> Eq for AstNodeRef<T> {}
impl<T> Hash for AstNodeRef<T> impl<T> Hash for AstNodeRef<T> {
where
T: Hash,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.node().hash(state); self.node.hash(state);
} }
} }
@ -117,7 +133,7 @@ mod tests {
let stmt_cloned = &cloned.syntax().body[0]; let stmt_cloned = &cloned.syntax().body[0];
let cloned_node = unsafe { AstNodeRef::new(cloned.clone(), stmt_cloned) }; let cloned_node = unsafe { AstNodeRef::new(cloned.clone(), stmt_cloned) };
assert_eq!(node1, cloned_node); assert_ne!(node1, cloned_node);
let other_raw = parse_unchecked_source("2 + 2", PySourceType::Python); let other_raw = parse_unchecked_source("2 + 2", PySourceType::Python);
let other = ParsedModule::new(other_raw); let other = ParsedModule::new(other_raw);

View file

@ -133,7 +133,7 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) struct SearchPaths { pub struct SearchPaths {
/// Search paths that have been statically determined purely from reading Ruff's configuration settings. /// Search paths that have been statically determined purely from reading Ruff's configuration settings.
/// These shouldn't ever change unless the config settings themselves change. /// These shouldn't ever change unless the config settings themselves change.
static_paths: Vec<SearchPath>, static_paths: Vec<SearchPath>,

View file

@ -1,13 +1,14 @@
use std::iter::FusedIterator; use std::iter::FusedIterator;
use std::sync::Arc; use std::sync::Arc;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_index::{IndexSlice, IndexVec}; use ruff_index::{IndexSlice, IndexVec};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
use salsa::Update;
use crate::module_name::ModuleName; use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds; use crate::semantic_index::ast_ids::AstIds;
@ -123,7 +124,7 @@ pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
} }
/// The symbol tables and use-def maps for all scopes in a file. /// The symbol tables and use-def maps for all scopes in a file.
#[derive(Debug)] #[derive(Debug, Update)]
pub(crate) struct SemanticIndex<'db> { pub(crate) struct SemanticIndex<'db> {
/// List of all symbol tables in this file, indexed by scope. /// List of all symbol tables in this file, indexed by scope.
symbol_tables: IndexVec<FileScopeId, Arc<SymbolTable>>, symbol_tables: IndexVec<FileScopeId, Arc<SymbolTable>>,

View file

@ -24,7 +24,7 @@ use crate::Db;
/// ///
/// x = foo() /// x = foo()
/// ``` /// ```
#[derive(Debug)] #[derive(Debug, salsa::Update)]
pub(crate) struct AstIds { pub(crate) struct AstIds {
/// Maps expressions to their expression id. /// Maps expressions to their expression id.
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>, expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
@ -74,6 +74,7 @@ impl HasScopedUseId for ast::ExprRef<'_> {
/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`]. /// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`].
#[newtype_index] #[newtype_index]
#[derive(salsa::Update)]
pub struct ScopedExpressionId; pub struct ScopedExpressionId;
pub trait HasScopedExpressionId { pub trait HasScopedExpressionId {
@ -181,7 +182,7 @@ pub(crate) mod node_key {
use crate::node_key::NodeKey; use crate::node_key::NodeKey;
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)]
pub(crate) struct ExpressionNodeKey(NodeKey); pub(crate) struct ExpressionNodeKey(NodeKey);
impl From<ast::ExprRef<'_>> for ExpressionNodeKey { impl From<ast::ExprRef<'_>> for ExpressionNodeKey {

View file

@ -9,7 +9,7 @@ use rustc_hash::FxHashMap;
/// Describes an (annotated) attribute assignment that we discovered in a method /// Describes an (annotated) attribute assignment that we discovered in a method
/// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`. /// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(crate) enum AttributeAssignment<'db> { pub(crate) enum AttributeAssignment<'db> {
/// An attribute assignment with an explicit type annotation, either /// An attribute assignment with an explicit type annotation, either
/// `self.x: <annotation>` or `self.x: <annotation> = …`. /// `self.x: <annotation>` or `self.x: <annotation> = …`.

View file

@ -5,20 +5,20 @@ use crate::db::Db;
use crate::semantic_index::expression::Expression; use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId}; use crate::semantic_index::symbol::{FileScopeId, ScopeId};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) struct Constraint<'db> { pub(crate) struct Constraint<'db> {
pub(crate) node: ConstraintNode<'db>, pub(crate) node: ConstraintNode<'db>,
pub(crate) is_positive: bool, pub(crate) is_positive: bool,
} }
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) enum ConstraintNode<'db> { pub(crate) enum ConstraintNode<'db> {
Expression(Expression<'db>), Expression(Expression<'db>),
Pattern(PatternConstraint<'db>), Pattern(PatternConstraint<'db>),
} }
/// Pattern kinds for which we support type narrowing and/or static visibility analysis. /// Pattern kinds for which we support type narrowing and/or static visibility analysis.
#[derive(Debug, Clone, Hash, PartialEq)] #[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
pub(crate) enum PatternConstraintKind<'db> { pub(crate) enum PatternConstraintKind<'db> {
Singleton(Singleton, Option<Expression<'db>>), Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>), Value(Expression<'db>, Option<Expression<'db>>),
@ -28,21 +28,15 @@ pub(crate) enum PatternConstraintKind<'db> {
#[salsa::tracked] #[salsa::tracked]
pub(crate) struct PatternConstraint<'db> { pub(crate) struct PatternConstraint<'db> {
#[id]
pub(crate) file: File, pub(crate) file: File,
#[id]
pub(crate) file_scope: FileScopeId, pub(crate) file_scope: FileScopeId,
#[no_eq]
#[return_ref]
pub(crate) subject: Expression<'db>, pub(crate) subject: Expression<'db>,
#[no_eq]
#[return_ref] #[return_ref]
pub(crate) kind: PatternConstraintKind<'db>, pub(crate) kind: PatternConstraintKind<'db>,
#[no_eq]
count: countme::Count<PatternConstraint<'static>>, count: countme::Count<PatternConstraint<'static>>,
} }

View file

@ -25,22 +25,19 @@ use crate::Db;
#[salsa::tracked] #[salsa::tracked]
pub struct Definition<'db> { pub struct Definition<'db> {
/// The file in which the definition occurs. /// The file in which the definition occurs.
#[id]
pub(crate) file: File, pub(crate) file: File,
/// The scope in which the definition occurs. /// The scope in which the definition occurs.
#[id]
pub(crate) file_scope: FileScopeId, pub(crate) file_scope: FileScopeId,
/// The symbol defined. /// The symbol defined.
#[id]
pub(crate) symbol: ScopedSymbolId, pub(crate) symbol: ScopedSymbolId,
#[no_eq] #[no_eq]
#[return_ref] #[return_ref]
#[tracked]
pub(crate) kind: DefinitionKind<'db>, pub(crate) kind: DefinitionKind<'db>,
#[no_eq]
count: countme::Count<Definition<'static>>, count: countme::Count<Definition<'static>>,
} }
@ -435,7 +432,14 @@ impl DefinitionCategory {
} }
} }
#[derive(Clone, Debug)] /// The kind of a definition.
///
/// ## Usage in salsa tracked structs
///
/// [`DefinitionKind`] fields in salsa tracked structs should be tracked (attributed with `#[tracked]`)
/// because the kind is a thin wrapper around [`AstNodeRef`]. See the [`AstNodeRef`] documentation
/// for an in-depth explanation of why this is necessary.
#[derive(Clone, Debug, Hash)]
pub enum DefinitionKind<'db> { pub enum DefinitionKind<'db> {
Import(AstNodeRef<ast::Alias>), Import(AstNodeRef<ast::Alias>),
ImportFrom(ImportFromDefinitionKind), ImportFrom(ImportFromDefinitionKind),
@ -540,7 +544,7 @@ impl DefinitionKind<'_> {
} }
} }
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq, Hash)]
pub(crate) enum TargetKind<'db> { pub(crate) enum TargetKind<'db> {
Sequence(Unpack<'db>), Sequence(Unpack<'db>),
Name, Name,
@ -555,7 +559,7 @@ impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct MatchPatternDefinitionKind { pub struct MatchPatternDefinitionKind {
pattern: AstNodeRef<ast::Pattern>, pattern: AstNodeRef<ast::Pattern>,
@ -573,7 +577,7 @@ impl MatchPatternDefinitionKind {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
pub struct ComprehensionDefinitionKind { pub struct ComprehensionDefinitionKind {
iterable: AstNodeRef<ast::Expr>, iterable: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::ExprName>, target: AstNodeRef<ast::ExprName>,
@ -599,7 +603,7 @@ impl ComprehensionDefinitionKind {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
pub struct ImportFromDefinitionKind { pub struct ImportFromDefinitionKind {
node: AstNodeRef<ast::StmtImportFrom>, node: AstNodeRef<ast::StmtImportFrom>,
alias_index: usize, alias_index: usize,
@ -615,7 +619,7 @@ impl ImportFromDefinitionKind {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
pub struct AssignmentDefinitionKind<'db> { pub struct AssignmentDefinitionKind<'db> {
target: TargetKind<'db>, target: TargetKind<'db>,
value: AstNodeRef<ast::Expr>, value: AstNodeRef<ast::Expr>,
@ -641,7 +645,7 @@ impl<'db> AssignmentDefinitionKind<'db> {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
pub struct WithItemDefinitionKind { pub struct WithItemDefinitionKind {
node: AstNodeRef<ast::WithItem>, node: AstNodeRef<ast::WithItem>,
target: AstNodeRef<ast::ExprName>, target: AstNodeRef<ast::ExprName>,
@ -662,7 +666,7 @@ impl WithItemDefinitionKind {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
pub struct ForStmtDefinitionKind<'db> { pub struct ForStmtDefinitionKind<'db> {
target: TargetKind<'db>, target: TargetKind<'db>,
iterable: AstNodeRef<ast::Expr>, iterable: AstNodeRef<ast::Expr>,
@ -693,7 +697,7 @@ impl<'db> ForStmtDefinitionKind<'db> {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash)]
pub struct ExceptHandlerDefinitionKind { pub struct ExceptHandlerDefinitionKind {
handler: AstNodeRef<ast::ExceptHandlerExceptHandler>, handler: AstNodeRef<ast::ExceptHandlerExceptHandler>,
is_star: bool, is_star: bool,
@ -713,7 +717,7 @@ impl ExceptHandlerDefinitionKind {
} }
} }
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)]
pub(crate) struct DefinitionNodeKey(NodeKey); pub(crate) struct DefinitionNodeKey(NodeKey);
impl From<&ast::Alias> for DefinitionNodeKey { impl From<&ast::Alias> for DefinitionNodeKey {

View file

@ -33,23 +33,20 @@ pub(crate) enum ExpressionKind {
#[salsa::tracked] #[salsa::tracked]
pub(crate) struct Expression<'db> { pub(crate) struct Expression<'db> {
/// The file in which the expression occurs. /// The file in which the expression occurs.
#[id]
pub(crate) file: File, pub(crate) file: File,
/// The scope in which the expression occurs. /// The scope in which the expression occurs.
#[id]
pub(crate) file_scope: FileScopeId, pub(crate) file_scope: FileScopeId,
/// The expression node. /// The expression node.
#[no_eq] #[no_eq]
#[tracked]
#[return_ref] #[return_ref]
pub(crate) node_ref: AstNodeRef<ast::Expr>, pub(crate) node_ref: AstNodeRef<ast::Expr>,
/// Should this expression be inferred as a normal expression or a type expression? /// Should this expression be inferred as a normal expression or a type expression?
#[id]
pub(crate) kind: ExpressionKind, pub(crate) kind: ExpressionKind,
#[no_eq]
count: countme::Count<Expression<'static>>, count: countme::Count<Expression<'static>>,
} }

View file

@ -96,18 +96,16 @@ impl From<FileSymbolId> for ScopedSymbolId {
/// Symbol ID that uniquely identifies a symbol inside a [`Scope`]. /// Symbol ID that uniquely identifies a symbol inside a [`Scope`].
#[newtype_index] #[newtype_index]
#[derive(salsa::Update)]
pub struct ScopedSymbolId; pub struct ScopedSymbolId;
/// A cross-module identifier of a scope that can be used as a salsa query parameter. /// A cross-module identifier of a scope that can be used as a salsa query parameter.
#[salsa::tracked] #[salsa::tracked]
pub struct ScopeId<'db> { pub struct ScopeId<'db> {
#[id]
pub file: File, pub file: File,
#[id]
pub file_scope_id: FileScopeId, pub file_scope_id: FileScopeId,
#[no_eq]
count: countme::Count<ScopeId<'static>>, count: countme::Count<ScopeId<'static>>,
} }
@ -159,6 +157,7 @@ impl<'db> ScopeId<'db> {
/// ID that uniquely identifies a scope inside of a module. /// ID that uniquely identifies a scope inside of a module.
#[newtype_index] #[newtype_index]
#[derive(salsa::Update)]
pub struct FileScopeId; pub struct FileScopeId;
impl FileScopeId { impl FileScopeId {
@ -177,7 +176,7 @@ impl FileScopeId {
} }
} }
#[derive(Debug)] #[derive(Debug, salsa::Update)]
pub struct Scope { pub struct Scope {
pub(super) parent: Option<FileScopeId>, pub(super) parent: Option<FileScopeId>,
pub(super) node: NodeWithScopeKind, pub(super) node: NodeWithScopeKind,
@ -216,7 +215,7 @@ impl ScopeKind {
} }
/// Symbol table for a specific [`Scope`]. /// Symbol table for a specific [`Scope`].
#[derive(Debug, Default)] #[derive(Debug, Default, salsa::Update)]
pub struct SymbolTable { pub struct SymbolTable {
/// The symbols in this scope. /// The symbols in this scope.
symbols: IndexVec<ScopedSymbolId, Symbol>, symbols: IndexVec<ScopedSymbolId, Symbol>,
@ -424,7 +423,7 @@ impl NodeWithScopeRef<'_> {
} }
/// Node that introduces a new scope. /// Node that introduces a new scope.
#[derive(Clone, Debug)] #[derive(Clone, Debug, salsa::Update)]
pub enum NodeWithScopeKind { pub enum NodeWithScopeKind {
Module, Module,
Class(AstNodeRef<ast::StmtClassDef>), Class(AstNodeRef<ast::StmtClassDef>),

View file

@ -278,7 +278,7 @@ mod symbol_state;
type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>; type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
/// Applicable definitions and constraints for every use of a name. /// Applicable definitions and constraints for every use of a name.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct UseDefMap<'db> { pub(crate) struct UseDefMap<'db> {
/// Array of [`Definition`] in this scope. Only the first entry should be `None`; /// Array of [`Definition`] in this scope. Only the first entry should be `None`;
/// this represents the implicit "unbound"/"undeclared" definition of every symbol. /// this represents the implicit "unbound"/"undeclared" definition of every symbol.
@ -384,7 +384,7 @@ impl<'db> UseDefMap<'db> {
} }
/// Either live bindings or live declarations for a symbol. /// Either live bindings or live declarations for a symbol.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, salsa::Update)]
enum SymbolDefinitions { enum SymbolDefinitions {
Bindings(SymbolBindings), Bindings(SymbolBindings),
Declarations(SymbolDeclarations), Declarations(SymbolDeclarations),

View file

@ -26,7 +26,7 @@ pub(crate) enum Boundness {
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound), /// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
/// non_existent: Symbol::Unbound, /// non_existent: Symbol::Unbound,
/// ``` /// ```
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(crate) enum Symbol<'db> { pub(crate) enum Symbol<'db> {
Type(Type<'db>, Boundness), Type(Type<'db>, Boundness),
Unbound, Unbound,

View file

@ -56,7 +56,6 @@ mod mro;
mod narrow; mod narrow;
mod signatures; mod signatures;
mod slots; mod slots;
mod statistics;
mod string_annotation; mod string_annotation;
mod subclass_of; mod subclass_of;
mod type_ordering; mod type_ordering;
@ -2589,7 +2588,7 @@ bitflags! {
/// ///
/// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the /// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the
/// qualifier `ClassVar`. /// qualifier `ClassVar`.
#[derive(Clone, Debug, Copy, Eq, PartialEq)] #[derive(Clone, Debug, Copy, Eq, PartialEq, salsa::Update)]
pub(crate) struct TypeAndQualifiers<'db> { pub(crate) struct TypeAndQualifiers<'db> {
inner: Type<'db>, inner: Type<'db>,
qualifiers: TypeQualifiers, qualifiers: TypeQualifiers,
@ -4400,7 +4399,7 @@ impl<'db> TypeAliasType<'db> {
} }
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) struct MetaclassCandidate<'db> { pub(super) struct MetaclassCandidate<'db> {
metaclass: Class<'db>, metaclass: Class<'db>,
explicit_metaclass_of: Class<'db>, explicit_metaclass_of: Class<'db>,
@ -4443,7 +4442,7 @@ impl<'db> From<InstanceType<'db>> for Type<'db> {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) struct MetaclassError<'db> { pub(super) struct MetaclassError<'db> {
kind: MetaclassErrorKind<'db>, kind: MetaclassErrorKind<'db>,
} }
@ -4455,7 +4454,7 @@ impl<'db> MetaclassError<'db> {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) enum MetaclassErrorKind<'db> { pub(super) enum MetaclassErrorKind<'db> {
/// The class has incompatible metaclasses in its inheritance hierarchy. /// The class has incompatible metaclasses in its inheritance hierarchy.
/// ///

View file

@ -61,7 +61,6 @@ use crate::types::diagnostic::{
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
}; };
use crate::types::mro::MroErrorKind; use crate::types::mro::MroErrorKind;
use crate::types::statistics::TypeStatistics;
use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{ use crate::types::{
builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations, builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
@ -237,7 +236,7 @@ impl<'db> InferenceRegion<'db> {
} }
/// The inferred types for a single region. /// The inferred types for a single region.
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq, salsa::Update)]
pub(crate) struct TypeInference<'db> { pub(crate) struct TypeInference<'db> {
/// The types of every expression in this region. /// The types of every expression in this region.
expressions: FxHashMap<ScopedExpressionId, Type<'db>>, expressions: FxHashMap<ScopedExpressionId, Type<'db>>,
@ -300,14 +299,6 @@ impl<'db> TypeInference<'db> {
self.diagnostics.shrink_to_fit(); self.diagnostics.shrink_to_fit();
self.deferred.shrink_to_fit(); self.deferred.shrink_to_fit();
} }
pub(super) fn statistics(&self) -> TypeStatistics {
let mut statistics = TypeStatistics::default();
for ty in self.expressions.values() {
statistics.increment(*ty);
}
statistics
}
} }
impl WithDiagnostics for TypeInference<'_> { impl WithDiagnostics for TypeInference<'_> {
@ -6433,6 +6424,7 @@ mod tests {
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events() db.take_salsa_events()
}; };
assert_function_query_was_not_run( assert_function_query_was_not_run(
&db, &db,
infer_expression_types, infer_expression_types,

View file

@ -10,7 +10,7 @@ use crate::Db;
/// The inferred method resolution order of a given class. /// The inferred method resolution order of a given class.
/// ///
/// See [`Class::iter_mro`] for more details. /// See [`Class::iter_mro`] for more details.
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug, salsa::Update)]
pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>); pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
impl<'db> Mro<'db> { impl<'db> Mro<'db> {
@ -236,7 +236,7 @@ impl<'db> Iterator for MroIterator<'db> {
impl std::iter::FusedIterator for MroIterator<'_> {} impl std::iter::FusedIterator for MroIterator<'_> {}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(super) struct MroError<'db> { pub(super) struct MroError<'db> {
kind: MroErrorKind<'db>, kind: MroErrorKind<'db>,
fallback_mro: Mro<'db>, fallback_mro: Mro<'db>,
@ -256,7 +256,7 @@ impl<'db> MroError<'db> {
} }
/// Possible ways in which attempting to resolve the MRO of a class might fail. /// Possible ways in which attempting to resolve the MRO of a class might fail.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(super) enum MroErrorKind<'db> { pub(super) enum MroErrorKind<'db> {
/// The class inherits from one or more invalid bases. /// The class inherits from one or more invalid bases.
/// ///

View file

@ -231,10 +231,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
match pattern.kind(self.db) { match pattern.kind(self.db) {
PatternConstraintKind::Singleton(singleton, _guard) => { PatternConstraintKind::Singleton(singleton, _guard) => {
self.evaluate_match_pattern_singleton(*subject, *singleton) self.evaluate_match_pattern_singleton(subject, *singleton)
} }
PatternConstraintKind::Class(cls, _guard) => { PatternConstraintKind::Class(cls, _guard) => {
self.evaluate_match_pattern_class(*subject, *cls) self.evaluate_match_pattern_class(subject, *cls)
} }
// TODO: support more pattern kinds // TODO: support more pattern kinds
PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None, PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None,

View file

@ -4,7 +4,7 @@ use crate::{semantic_index::definition::Definition, types::todo_type};
use ruff_python_ast::{self as ast, name::Name}; use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature. /// A typed callable signature.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct Signature<'db> { pub(crate) struct Signature<'db> {
/// Parameters, in source order. /// Parameters, in source order.
/// ///
@ -60,7 +60,7 @@ impl<'db> Signature<'db> {
} }
// TODO: use SmallVec here once invariance bug is fixed // TODO: use SmallVec here once invariance bug is fixed
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>); pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
impl<'db> Parameters<'db> { impl<'db> Parameters<'db> {
@ -218,7 +218,7 @@ impl<'db> std::ops::Index<usize> for Parameters<'db> {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct Parameter<'db> { pub(crate) struct Parameter<'db> {
/// Parameter name. /// Parameter name.
/// ///
@ -304,7 +304,7 @@ impl<'db> Parameter<'db> {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub(crate) enum ParameterKind<'db> { pub(crate) enum ParameterKind<'db> {
/// Positional-only parameter, e.g. `def f(x, /): ...` /// Positional-only parameter, e.g. `def f(x, /): ...`
PositionalOnly { default_ty: Option<Type<'db>> }, PositionalOnly { default_ty: Option<Type<'db>> },

View file

@ -1,121 +0,0 @@
use crate::types::{infer_scope_types, semantic_index, Type};
use crate::Db;
use ruff_db::files::File;
use rustc_hash::FxHashMap;
/// Get type-coverage statistics for a file.
#[salsa::tracked(return_ref)]
pub fn type_statistics<'db>(db: &'db dyn Db, file: File) -> TypeStatistics<'db> {
let _span = tracing::trace_span!("type_statistics", file=?file.path(db)).entered();
tracing::debug!(
"Gathering statistics for file '{path}'",
path = file.path(db)
);
let index = semantic_index(db, file);
let mut statistics = TypeStatistics::default();
for scope_id in index.scope_ids() {
let result = infer_scope_types(db, scope_id);
statistics.extend(&result.statistics());
}
statistics
}
/// Map each type to count of expressions with that type.
#[derive(Debug, Default, Eq, PartialEq)]
pub(super) struct TypeStatistics<'db>(FxHashMap<Type<'db>, u32>);
impl<'db> TypeStatistics<'db> {
fn extend(&mut self, other: &TypeStatistics<'db>) {
for (ty, count) in &other.0 {
self.0
.entry(*ty)
.and_modify(|my_count| *my_count += count)
.or_insert(*count);
}
}
pub(super) fn increment(&mut self, ty: Type<'db>) {
self.0
.entry(ty)
.and_modify(|count| *count += 1)
.or_insert(1);
}
#[allow(unused)]
fn expression_count(&self) -> u32 {
self.0.values().sum()
}
#[allow(unused)]
fn todo_count(&self) -> u32 {
self.0
.iter()
.filter(|(key, _)| key.is_todo())
.map(|(_, count)| count)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
fn get_stats<'db>(
db: &'db mut TestDb,
filename: &str,
source: &str,
) -> &'db TypeStatistics<'db> {
db.write_dedented(filename, source).unwrap();
type_statistics(db, system_path_to_file(db, filename).unwrap())
}
#[test]
fn all_static() {
let mut db = setup_db();
let stats = get_stats(&mut db, "src/foo.py", "1");
assert_eq!(stats.0, FxHashMap::from_iter([(Type::IntLiteral(1), 1)]));
}
#[test]
fn todo_and_expression_count() {
let mut db = setup_db();
let stats = get_stats(
&mut db,
"src/foo.py",
r#"
x = [x for x in [1]]
"#,
);
assert_eq!(stats.todo_count(), 4);
assert_eq!(stats.expression_count(), 6);
}
#[test]
fn sum() {
let mut db = setup_db();
let stats = get_stats(
&mut db,
"src/foo.py",
r#"
1
def f():
1
"#,
);
assert_eq!(stats.0[&Type::IntLiteral(1)], 2);
}
}

View file

@ -261,7 +261,7 @@ impl<'db> Unpacker<'db> {
} }
} }
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq, salsa::Update)]
pub(crate) struct UnpackResult<'db> { pub(crate) struct UnpackResult<'db> {
targets: FxHashMap<ScopedExpressionId, Type<'db>>, targets: FxHashMap<ScopedExpressionId, Type<'db>>,
diagnostics: TypeCheckDiagnostics, diagnostics: TypeCheckDiagnostics,

View file

@ -28,10 +28,8 @@ use crate::Db;
/// * an argument of a cross-module query /// * an argument of a cross-module query
#[salsa::tracked] #[salsa::tracked]
pub(crate) struct Unpack<'db> { pub(crate) struct Unpack<'db> {
#[id]
pub(crate) file: File, pub(crate) file: File,
#[id]
pub(crate) file_scope: FileScopeId, pub(crate) file_scope: FileScopeId,
/// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target /// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target
@ -45,7 +43,6 @@ pub(crate) struct Unpack<'db> {
#[no_eq] #[no_eq]
pub(crate) value: UnpackValue<'db>, pub(crate) value: UnpackValue<'db>,
#[no_eq]
count: countme::Count<Unpack<'static>>, count: countme::Count<Unpack<'static>>,
} }
@ -62,7 +59,7 @@ impl<'db> Unpack<'db> {
} }
/// The expression that is being unpacked. /// The expression that is being unpacked.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Hash)]
pub(crate) enum UnpackValue<'db> { pub(crate) enum UnpackValue<'db> {
/// An iterable expression like the one in a `for` loop or a comprehension. /// An iterable expression like the one in a `for` loop or a comprehension.
Iterable(Expression<'db>), Iterable(Expression<'db>),

View file

@ -338,7 +338,7 @@ const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
/// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we /// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we
/// maintain a separate set of visibility constraints for each scope in file. /// maintain a separate set of visibility constraints for each scope in file.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct VisibilityConstraints<'db> { pub(crate) struct VisibilityConstraints<'db> {
constraints: IndexVec<Atom, Constraint<'db>>, constraints: IndexVec<Atom, Constraint<'db>>,
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>, interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
@ -627,7 +627,7 @@ impl<'db> VisibilityConstraints<'db> {
ConstraintNode::Pattern(inner) => match inner.kind(db) { ConstraintNode::Pattern(inner) => match inner.kind(db) {
PatternConstraintKind::Value(value, guard) => { PatternConstraintKind::Value(value, guard) => {
let subject_expression = inner.subject(db); let subject_expression = inner.subject(db);
let inference = infer_expression_types(db, *subject_expression); let inference = infer_expression_types(db, subject_expression);
let scope = subject_expression.scope(db); let scope = subject_expression.scope(db);
let subject_ty = inference.expression_type( let subject_ty = inference.expression_type(
subject_expression subject_expression

View file

@ -73,6 +73,14 @@ impl std::fmt::Debug for ParsedModule {
} }
} }
impl PartialEq for ParsedModule {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
impl Eq for ParsedModule {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::files::{system_path_to_file, vendored_path_to_file}; use crate::files::{system_path_to_file, vendored_path_to_file};

View file

@ -18,7 +18,7 @@ pub fn assert_function_query_was_not_run<Db, Q, QDb, I, R>(
db.attach(|_| { db.attach(|_| {
if let Some(will_execute_event) = will_execute_event { if let Some(will_execute_event) = will_execute_event {
panic!("Expected query {query_name}({id}) not to have run but it did: {will_execute_event:?}"); panic!("Expected query {query_name}({id}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}");
} }
}); });
} }
@ -46,7 +46,7 @@ pub fn assert_const_function_query_was_not_run<Db, Q, QDb, R>(
db.attach(|_| { db.attach(|_| {
if let Some(will_execute_event) = event { if let Some(will_execute_event) = event {
panic!( panic!(
"Expected query {query_name}() not to have run but it did: {will_execute_event:?}" "Expected query {query_name}() not to have run but it did: {will_execute_event:?}\n\n{events:#?}"
); );
} }
}); });

View file

@ -15,6 +15,7 @@ doctest = false
[dependencies] [dependencies]
ruff_macros = { workspace = true } ruff_macros = { workspace = true }
salsa = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
static_assertions = { workspace = true } static_assertions = { workspace = true }

View file

@ -181,3 +181,16 @@ impl<I: Idx, T, const N: usize> From<[T; N]> for IndexVec<I, T> {
// not the phantom data. // not the phantom data.
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe impl<I: Idx, T> Send for IndexVec<I, T> where T: Send {} unsafe impl<I: Idx, T> Send for IndexVec<I, T> where T: Send {}
#[allow(unsafe_code)]
#[cfg(feature = "salsa")]
unsafe impl<I, T> salsa::Update for IndexVec<I, T>
where
T: salsa::Update,
{
#[allow(unsafe_code)]
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_vec: &mut IndexVec<I, T> = unsafe { &mut *old_pointer };
salsa::Update::maybe_update(&mut old_vec.raw, new_value.raw)
}
}

View file

@ -26,6 +26,7 @@ is-macro = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
memchr = { workspace = true } memchr = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
salsa = { workspace = true, optional = true }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }

View file

@ -8,6 +8,7 @@ use crate::{nodes, Expr};
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))] #[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))]
#[cfg_attr(feature = "salsa", derive(salsa::Update))]
pub struct Name(compact_str::CompactString); pub struct Name(compact_str::CompactString);
impl Name { impl Name {

View file

@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" }
ruff_text_size = { path = "../crates/ruff_text_size" } ruff_text_size = { path = "../crates/ruff_text_size" }
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" } salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
similar = { version = "2.5.0" } similar = { version = "2.5.0" }
tracing = { version = "0.1.40" } tracing = { version = "0.1.40" }