Skip to content

Commit 50020fb

Browse files
authored
[ty] Avoid storing constraint nodes twice (#28375)
1 parent 446bb68 commit 50020fb

4 files changed

Lines changed: 108 additions & 20 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Data structures for interning interior nodes.
2+
//!
3+
//! Each graph needs stable IDs for its nodes and a way to reuse an existing ID when the same
4+
//! node is constructed again. The indexed vector owns the nodes; a hash table of IDs provides
5+
//! lookup without storing another copy of each node.
6+
7+
use std::hash::{BuildHasher, Hash};
8+
use std::ops::Index;
9+
10+
use hashbrown::{HashTable, hash_table::Entry};
11+
use ruff_index::{Idx, IndexVec};
12+
use rustc_hash::FxBuildHasher;
13+
14+
/// An indexed collection of distinct nodes with a reverse table of their IDs.
15+
///
16+
/// The vector owns each node at the index given by its stable ID. The hash table stores only
17+
/// IDs, hashing and comparing them by reading the corresponding nodes from the vector. Both
18+
/// collections are private, and [`Self::intern`] updates them together: every table entry
19+
/// refers to a node in the vector, and an equal node reuses that node's ID. Once the graph is
20+
/// built, [`Self::into_nodes_boxed_slice`] returns a boxed slice of nodes and drops the table.
21+
#[derive(Debug)]
22+
pub(crate) struct InternedNodes<I: Idx, N> {
23+
nodes: IndexVec<I, N>,
24+
ids: HashTable<I>,
25+
}
26+
27+
impl<I: Idx, N> Default for InternedNodes<I, N> {
28+
fn default() -> Self {
29+
Self {
30+
nodes: IndexVec::default(),
31+
ids: HashTable::default(),
32+
}
33+
}
34+
}
35+
36+
impl<I: Idx, N> InternedNodes<I, N> {
37+
pub(crate) const fn len(&self) -> usize {
38+
self.nodes.raw.len()
39+
}
40+
41+
/// Consume `self` and return an iterator over the interned nodes.
42+
pub(crate) fn into_node_iterator(self) -> impl Iterator<Item = N> {
43+
self.nodes.into_iter()
44+
}
45+
46+
/// Consume `self` and return a boxed slice of the interned nodes.
47+
pub(crate) fn into_nodes_boxed_slice(self) -> Box<[N]> {
48+
self.nodes.raw.into_boxed_slice()
49+
}
50+
}
51+
52+
impl<I: Idx, N: Eq + Hash> InternedNodes<I, N> {
53+
pub(crate) fn find(&self, node: &N) -> Option<I> {
54+
self.ids
55+
.find(FxBuildHasher.hash_one(node), |id| self.nodes[*id].eq(node))
56+
.copied()
57+
}
58+
59+
/// Returns the node ID and whether the node was newly inserted.
60+
pub(crate) fn intern(&mut self, node: N) -> (I, bool) {
61+
let nodes = &mut self.nodes;
62+
match self.ids.entry(
63+
FxBuildHasher.hash_one(&node),
64+
|id| nodes[*id].eq(&node),
65+
|id| FxBuildHasher.hash_one(&nodes[*id]),
66+
) {
67+
Entry::Occupied(entry) => (*entry.get(), false),
68+
Entry::Vacant(entry) => {
69+
let id = nodes.push(node);
70+
entry.insert(id);
71+
(id, true)
72+
}
73+
}
74+
}
75+
}
76+
77+
impl<I: Idx, N> Index<I> for InternedNodes<I, N> {
78+
type Output = N;
79+
80+
fn index(&self, id: I) -> &N {
81+
&self.nodes[id]
82+
}
83+
}

‎crates/ty_python_core/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mod db;
4545
pub mod definition;
4646
pub mod expression;
4747
pub mod frozen;
48+
mod interned_nodes;
4849
pub(crate) mod member;
4950
pub mod narrowing_constraints;
5051
pub mod node_key;

‎crates/ty_python_core/src/narrowing_constraints.rs‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@
3434
3535
use std::cmp::Ordering;
3636

37-
use ruff_index::{Idx, IndexVec};
37+
use ruff_index::Idx;
3838
use rustc_hash::FxHashMap;
3939

4040
use crate::ast_ids::ScopedUseId;
41+
use crate::interned_nodes::InternedNodes;
4142
use crate::predicate::ScopedPredicateId;
4243
use crate::rank::{RankBitBox, RankBitBoxVec};
4344
use crate::scope::FileScopeId;
@@ -128,11 +129,10 @@ impl NarrowingConstraints {
128129
}
129130
}
130131

131-
#[derive(Debug, Default, PartialEq, Eq)]
132+
#[derive(Debug, Default)]
132133
pub struct NarrowingConstraintsBuilder {
133-
interiors: IndexVec<ScopedNarrowingConstraint, InteriorNode>,
134+
interiors: InternedNodes<ScopedNarrowingConstraint, InteriorNode>,
134135
interior_used: RankBitBoxVec,
135-
interior_cache: FxHashMap<InteriorNode, ScopedNarrowingConstraint>,
136136
and_cache: FxHashMap<
137137
(ScopedNarrowingConstraint, ScopedNarrowingConstraint),
138138
ScopedNarrowingConstraint,
@@ -147,13 +147,13 @@ impl NarrowingConstraintsBuilder {
147147
pub(crate) fn build(self) -> NarrowingConstraints {
148148
if self.interior_used.first_zero().is_none() {
149149
NarrowingConstraints {
150-
used_interiors: self.interiors.raw.into_boxed_slice(),
150+
used_interiors: self.interiors.into_nodes_boxed_slice(),
151151
used_indices: None,
152152
}
153153
} else {
154154
let used_interiors = self
155155
.interiors
156-
.into_iter()
156+
.into_node_iterator()
157157
.zip(&self.interior_used)
158158
.filter_map(|(interior, used)| used.then_some(interior))
159159
.collect();
@@ -228,10 +228,11 @@ impl NarrowingConstraintsBuilder {
228228
});
229229
}
230230

231-
*self.interior_cache.entry(node).or_insert_with(|| {
231+
let (id, inserted) = self.interiors.intern(node);
232+
if inserted {
232233
self.interior_used.push(false);
233-
self.interiors.push(node)
234-
})
234+
}
235+
id
235236
}
236237

237238
pub(crate) fn add_atom(&mut self, predicate: ScopedPredicateId) -> ScopedNarrowingConstraint {
@@ -280,8 +281,8 @@ impl NarrowingConstraintsBuilder {
280281
if_uncertain: ALWAYS_FALSE,
281282
if_false,
282283
};
283-
if let Some(cached) = self.interior_cache.get(&node) {
284-
return *cached;
284+
if let Some(cached) = self.interiors.find(&node) {
285+
return cached;
285286
}
286287
if self.interiors.len() >= MAX_INTERIOR_NODES {
287288
return ALWAYS_TRUE;

‎crates/ty_python_core/src/reachability_constraints.rs‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
55
use std::cmp::Ordering;
66

7-
use ruff_index::{Idx, IndexVec};
7+
use ruff_index::Idx;
88
use rustc_hash::FxHashMap;
99

10+
use crate::interned_nodes::InternedNodes;
1011
use crate::narrowing_constraints::{NarrowingConstraintsBuilder, ScopedNarrowingConstraint};
1112
use crate::predicate::ScopedPredicateId;
1213
use crate::rank::{RankBitBox, RankBitBoxVec};
@@ -172,11 +173,10 @@ impl ReachabilityConstraints {
172173
}
173174
}
174175

175-
#[derive(Debug, Default, PartialEq, Eq)]
176+
#[derive(Debug, Default)]
176177
pub struct ReachabilityConstraintsBuilder {
177-
interiors: IndexVec<ScopedReachabilityConstraintId, InteriorNode>,
178+
interiors: InternedNodes<ScopedReachabilityConstraintId, InteriorNode>,
178179
interior_used: RankBitBoxVec,
179-
interior_cache: FxHashMap<InteriorNode, ScopedReachabilityConstraintId>,
180180
not_cache: FxHashMap<ScopedReachabilityConstraintId, ScopedReachabilityConstraintId>,
181181
and_cache: FxHashMap<
182182
(
@@ -203,11 +203,13 @@ impl ReachabilityConstraintsBuilder {
203203
pub(crate) fn build(self) -> ReachabilityConstraints {
204204
if self.interior_used.first_zero().is_none() {
205205
ReachabilityConstraints {
206-
used_interiors: self.interiors.raw.into_boxed_slice(),
206+
used_interiors: self.interiors.into_nodes_boxed_slice(),
207207
used_indices: None,
208208
}
209209
} else {
210-
let used_interiors = (self.interiors.into_iter())
210+
let used_interiors = self
211+
.interiors
212+
.into_node_iterator()
211213
.zip(&self.interior_used)
212214
.filter_map(|(interior, used)| used.then_some(interior))
213215
.collect();
@@ -351,10 +353,11 @@ impl ReachabilityConstraintsBuilder {
351353
return node.if_true;
352354
}
353355

354-
*self.interior_cache.entry(node).or_insert_with(|| {
356+
let (id, inserted) = self.interiors.intern(node);
357+
if inserted {
355358
self.interior_used.push(false);
356-
self.interiors.push(node)
357-
})
359+
}
360+
id
358361
}
359362

360363
/// Adds a new reachability constraint that checks a single [`super::predicate::Predicate`].

0 commit comments

Comments
 (0)