Skip to content

Commit fa76f6c

Browse files
dcreagercarljm
andauthored
[red-knot] Use arena-allocated association lists for narrowing constraints (#16306)
This PR adds an implementation of [association lists](https://en.wikipedia.org/wiki/Association_list), and uses them to replace the previous `BitSet`/`SmallVec` representation for narrowing constraints. An association list is a linked list of key/value pairs. We additionally guarantee that the elements of an association list are sorted (by their keys), and that they do not contain any entries with duplicate keys. Association lists have fallen out of favor in recent decades, since you often need operations that are inefficient on them. In particular, looking up a random element by index is O(n), just like a linked list; and looking up an element by key is also O(n), since you must do a linear scan of the list to find the matching element. Luckily we don't need either of those operations for narrowing constraints! The typical implementation also suffers from poor cache locality and high memory allocation overhead, since individual list cells are typically allocated separately from the heap. We solve that last problem by storing the cells of an association list in an `IndexVec` arena. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
1 parent 5c007db commit fa76f6c

File tree

13 files changed

+1073
-322
lines changed

13 files changed

+1073
-322
lines changed

crates/red_knot_python_semantic/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ pub(crate) mod symbol;
2727
pub mod types;
2828
mod unpack;
2929
mod util;
30-
mod visibility_constraints;
3130

3231
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
3332

crates/red_knot_python_semantic/src/semantic_index.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ mod builder;
2828
pub(crate) mod constraint;
2929
pub mod definition;
3030
pub mod expression;
31+
mod narrowing_constraints;
3132
pub mod symbol;
3233
mod use_def;
34+
mod visibility_constraints;
3335

3436
pub(crate) use self::use_def::{
3537
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,

crates/red_knot_python_semantic/src/semantic_index/builder.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ use crate::module_name::ModuleName;
1515
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
1616
use crate::semantic_index::ast_ids::AstIdsBuilder;
1717
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
18-
use crate::semantic_index::constraint::{PatternConstraintKind, ScopedConstraintId};
18+
use crate::semantic_index::constraint::{
19+
Constraint, ConstraintNode, PatternConstraint, PatternConstraintKind, ScopedConstraintId,
20+
};
1921
use crate::semantic_index::definition::{
20-
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
21-
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
22+
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionCategory,
23+
DefinitionNodeKey, DefinitionNodeRef, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef,
24+
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
25+
WithItemDefinitionNodeRef,
2226
};
2327
use crate::semantic_index::expression::{Expression, ExpressionKind};
2428
use crate::semantic_index::symbol::{
@@ -28,17 +32,13 @@ use crate::semantic_index::symbol::{
2832
use crate::semantic_index::use_def::{
2933
EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder,
3034
};
35+
use crate::semantic_index::visibility_constraints::{
36+
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
37+
};
3138
use crate::semantic_index::SemanticIndex;
3239
use crate::unpack::{Unpack, UnpackValue};
33-
use crate::visibility_constraints::{ScopedVisibilityConstraintId, VisibilityConstraintsBuilder};
3440
use crate::Db;
3541

36-
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
37-
use super::definition::{
38-
DefinitionCategory, ExceptHandlerDefinitionNodeRef, ImportDefinitionNodeRef,
39-
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
40-
};
41-
4242
mod except_handlers;
4343

4444
/// Are we in a state where a `break` statement is allowed?
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//! # Narrowing constraints
2+
//!
3+
//! When building a semantic index for a file, we associate each binding with _narrowing
4+
//! constraints_. The narrowing constraint is used to constrain the type of the binding's symbol.
5+
//! Note that a binding can be associated with a different narrowing constraint at different points
6+
//! in a file. See the [`use_def`][crate::semantic_index::use_def] module for more details.
7+
//!
8+
//! This module defines how narrowing constraints are stored internally.
9+
//!
10+
//! A _narrowing constraint_ consists of a list of _clauses_, each of which corresponds with an
11+
//! expression in the source file (represented by a [`Constraint`]). We need to support the
12+
//! following operations on narrowing constraints:
13+
//!
14+
//! - Adding a new clause to an existing constraint
15+
//! - Merging two constraints together, which produces the _intersection_ of their clauses
16+
//! - Iterating through the clauses in a constraint
17+
//!
18+
//! In particular, note that we do not need random access to the clauses in a constraint. That
19+
//! means that we can use a simple [_sorted association list_][ruff_index::list] as our data
20+
//! structure. That lets us use a single 32-bit integer to store each narrowing constraint, no
21+
//! matter how many clauses it contains. It also makes merging two narrowing constraints fast,
22+
//! since alists support fast intersection.
23+
//!
24+
//! Because we visit the contents of each scope in source-file order, and assign scoped IDs in
25+
//! source-file order, that means that we will tend to visit narrowing constraints in order by
26+
//! their IDs. This is exactly how to get the best performance from our alist implementation.
27+
//!
28+
//! [`Constraint`]: crate::semantic_index::constraint::Constraint
29+
30+
use ruff_index::list::{ListBuilder, ListSetReverseIterator, ListStorage};
31+
use ruff_index::newtype_index;
32+
33+
use crate::semantic_index::constraint::ScopedConstraintId;
34+
35+
/// A narrowing constraint associated with a live binding.
36+
///
37+
/// A constraint is a list of clauses, each of which is a [`Constraint`] that constrains the type
38+
/// of the binding's symbol.
39+
///
40+
/// An instance of this type represents a _non-empty_ narrowing constraint. You will often wrap
41+
/// this in `Option` and use `None` to represent an empty narrowing constraint.
42+
///
43+
/// [`Constraint`]: crate::semantic_index::constraint::Constraint
44+
#[newtype_index]
45+
pub(crate) struct ScopedNarrowingConstraintId;
46+
47+
/// One of the clauses in a narrowing constraint, which is a [`Constraint`] that constrains the
48+
/// type of the binding's symbol.
49+
///
50+
/// Note that those [`Constraint`]s are stored in [their own per-scope
51+
/// arena][crate::semantic_index::constraint::Constraints], so internally we use a
52+
/// [`ScopedConstraintId`] to refer to the underlying constraint.
53+
///
54+
/// [`Constraint`]: crate::semantic_index::constraint::Constraint
55+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
56+
pub(crate) struct ScopedNarrowingConstraintClause(ScopedConstraintId);
57+
58+
impl ScopedNarrowingConstraintClause {
59+
/// Returns (the ID of) the `Constraint` for this clause
60+
pub(crate) fn constraint(self) -> ScopedConstraintId {
61+
self.0
62+
}
63+
}
64+
65+
impl From<ScopedConstraintId> for ScopedNarrowingConstraintClause {
66+
fn from(constraint: ScopedConstraintId) -> ScopedNarrowingConstraintClause {
67+
ScopedNarrowingConstraintClause(constraint)
68+
}
69+
}
70+
71+
/// A collection of narrowing constraints for a given scope.
72+
#[derive(Debug, Eq, PartialEq)]
73+
pub(crate) struct NarrowingConstraints {
74+
lists: ListStorage<ScopedNarrowingConstraintId, ScopedNarrowingConstraintClause>,
75+
}
76+
77+
// Building constraints
78+
// --------------------
79+
80+
/// A builder for creating narrowing constraints.
81+
#[derive(Debug, Default, Eq, PartialEq)]
82+
pub(crate) struct NarrowingConstraintsBuilder {
83+
lists: ListBuilder<ScopedNarrowingConstraintId, ScopedNarrowingConstraintClause>,
84+
}
85+
86+
impl NarrowingConstraintsBuilder {
87+
pub(crate) fn build(self) -> NarrowingConstraints {
88+
NarrowingConstraints {
89+
lists: self.lists.build(),
90+
}
91+
}
92+
93+
/// Adds a clause to an existing narrowing constraint.
94+
pub(crate) fn add(
95+
&mut self,
96+
constraint: Option<ScopedNarrowingConstraintId>,
97+
clause: ScopedNarrowingConstraintClause,
98+
) -> Option<ScopedNarrowingConstraintId> {
99+
self.lists.insert(constraint, clause)
100+
}
101+
102+
/// Returns the intersection of two narrowing constraints. The result contains the clauses that
103+
/// appear in both inputs.
104+
pub(crate) fn intersect(
105+
&mut self,
106+
a: Option<ScopedNarrowingConstraintId>,
107+
b: Option<ScopedNarrowingConstraintId>,
108+
) -> Option<ScopedNarrowingConstraintId> {
109+
self.lists.intersect(a, b)
110+
}
111+
}
112+
113+
// Iteration
114+
// ---------
115+
116+
pub(crate) type NarrowingConstraintsIterator<'a> = std::iter::Copied<
117+
ListSetReverseIterator<'a, ScopedNarrowingConstraintId, ScopedNarrowingConstraintClause>,
118+
>;
119+
120+
impl NarrowingConstraints {
121+
/// Iterates over the clauses in a narrowing constraint.
122+
pub(crate) fn iter_clauses(
123+
&self,
124+
set: Option<ScopedNarrowingConstraintId>,
125+
) -> NarrowingConstraintsIterator<'_> {
126+
self.lists.iter_set_reverse(set).copied()
127+
}
128+
}
129+
130+
// Test support
131+
// ------------
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
137+
impl ScopedNarrowingConstraintClause {
138+
pub(crate) fn as_u32(self) -> u32 {
139+
self.0.as_u32()
140+
}
141+
}
142+
143+
impl NarrowingConstraintsBuilder {
144+
pub(crate) fn iter_constraints(
145+
&self,
146+
set: Option<ScopedNarrowingConstraintId>,
147+
) -> NarrowingConstraintsIterator<'_> {
148+
self.lists.iter_set_reverse(set).copied()
149+
}
150+
}
151+
}

crates/red_knot_python_semantic/src/semantic_index/use_def.rs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,20 +260,22 @@ use ruff_index::{newtype_index, IndexVec};
260260
use rustc_hash::FxHashMap;
261261

262262
use self::symbol_state::{
263-
ConstraintIndexIterator, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator,
264-
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
263+
LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, ScopedDefinitionId,
264+
SymbolBindings, SymbolDeclarations, SymbolState,
265265
};
266266
use crate::semantic_index::ast_ids::ScopedUseId;
267267
use crate::semantic_index::constraint::{
268268
Constraint, Constraints, ConstraintsBuilder, ScopedConstraintId,
269269
};
270270
use crate::semantic_index::definition::Definition;
271+
use crate::semantic_index::narrowing_constraints::{
272+
NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator,
273+
};
271274
use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
272-
use crate::visibility_constraints::{
275+
use crate::semantic_index::visibility_constraints::{
273276
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
274277
};
275278

276-
mod bitset;
277279
mod symbol_state;
278280

279281
/// Applicable definitions and constraints for every use of a name.
@@ -286,6 +288,9 @@ pub(crate) struct UseDefMap<'db> {
286288
/// Array of [`Constraint`] in this scope.
287289
constraints: Constraints<'db>,
288290

291+
/// Array of narrowing constraints in this scope.
292+
narrowing_constraints: NarrowingConstraints,
293+
289294
/// Array of visibility constraints in this scope.
290295
visibility_constraints: VisibilityConstraints,
291296

@@ -370,6 +375,7 @@ impl<'db> UseDefMap<'db> {
370375
BindingWithConstraintsIterator {
371376
all_definitions: &self.all_definitions,
372377
constraints: &self.constraints,
378+
narrowing_constraints: &self.narrowing_constraints,
373379
visibility_constraints: &self.visibility_constraints,
374380
inner: bindings.iter(),
375381
}
@@ -416,6 +422,7 @@ type EagerBindings = IndexVec<ScopedEagerBindingsId, SymbolBindings>;
416422
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
417423
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
418424
pub(crate) constraints: &'map Constraints<'db>,
425+
pub(crate) narrowing_constraints: &'map NarrowingConstraints,
419426
pub(crate) visibility_constraints: &'map VisibilityConstraints,
420427
inner: LiveBindingsIterator<'map>,
421428
}
@@ -425,14 +432,16 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
425432

426433
fn next(&mut self) -> Option<Self::Item> {
427434
let constraints = self.constraints;
435+
let narrowing_constraints = self.narrowing_constraints;
428436

429437
self.inner
430438
.next()
431439
.map(|live_binding| BindingWithConstraints {
432440
binding: self.all_definitions[live_binding.binding],
433-
narrowing_constraints: ConstraintsIterator {
441+
narrowing_constraint: ConstraintsIterator {
434442
constraints,
435-
constraint_ids: live_binding.narrowing_constraints.iter(),
443+
constraint_ids: narrowing_constraints
444+
.iter_clauses(live_binding.narrowing_constraint),
436445
},
437446
visibility_constraint: live_binding.visibility_constraint,
438447
})
@@ -443,13 +452,13 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
443452

444453
pub(crate) struct BindingWithConstraints<'map, 'db> {
445454
pub(crate) binding: Option<Definition<'db>>,
446-
pub(crate) narrowing_constraints: ConstraintsIterator<'map, 'db>,
455+
pub(crate) narrowing_constraint: ConstraintsIterator<'map, 'db>,
447456
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
448457
}
449458

450459
pub(crate) struct ConstraintsIterator<'map, 'db> {
451460
constraints: &'map Constraints<'db>,
452-
constraint_ids: ConstraintIndexIterator<'map>,
461+
constraint_ids: NarrowingConstraintsIterator<'map>,
453462
}
454463

455464
impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
@@ -458,7 +467,7 @@ impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
458467
fn next(&mut self) -> Option<Self::Item> {
459468
self.constraint_ids
460469
.next()
461-
.map(|constraint_id| self.constraints[ScopedConstraintId::from_u32(constraint_id)])
470+
.map(|narrowing_constraint| self.constraints[narrowing_constraint.constraint()])
462471
}
463472
}
464473

@@ -509,7 +518,10 @@ pub(super) struct UseDefMapBuilder<'db> {
509518
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
510519

511520
/// Builder of constraints.
512-
constraints: ConstraintsBuilder<'db>,
521+
pub(super) constraints: ConstraintsBuilder<'db>,
522+
523+
/// Builder of narrowing constraints.
524+
pub(super) narrowing_constraints: NarrowingConstraintsBuilder,
513525

514526
/// Builder of visibility constraints.
515527
pub(super) visibility_constraints: VisibilityConstraintsBuilder,
@@ -542,6 +554,7 @@ impl Default for UseDefMapBuilder<'_> {
542554
Self {
543555
all_definitions: IndexVec::from_iter([None]),
544556
constraints: ConstraintsBuilder::default(),
557+
narrowing_constraints: NarrowingConstraintsBuilder::default(),
545558
visibility_constraints: VisibilityConstraintsBuilder::default(),
546559
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
547560
bindings_by_use: IndexVec::new(),
@@ -578,8 +591,9 @@ impl<'db> UseDefMapBuilder<'db> {
578591
}
579592

580593
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
594+
let narrowing_constraint = constraint.into();
581595
for state in &mut self.symbol_states {
582-
state.record_constraint(constraint);
596+
state.record_constraint(&mut self.narrowing_constraints, narrowing_constraint);
583597
}
584598
}
585599

@@ -737,10 +751,15 @@ impl<'db> UseDefMapBuilder<'db> {
737751
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
738752
for current in &mut self.symbol_states {
739753
if let Some(snapshot) = snapshot_definitions_iter.next() {
740-
current.merge(snapshot, &mut self.visibility_constraints);
754+
current.merge(
755+
snapshot,
756+
&mut self.narrowing_constraints,
757+
&mut self.visibility_constraints,
758+
);
741759
} else {
742760
current.merge(
743761
SymbolState::undefined(snapshot.scope_start_visibility),
762+
&mut self.narrowing_constraints,
744763
&mut self.visibility_constraints,
745764
);
746765
// Symbol not present in snapshot, so it's unbound/undeclared from that path.
@@ -763,6 +782,7 @@ impl<'db> UseDefMapBuilder<'db> {
763782
UseDefMap {
764783
all_definitions: self.all_definitions,
765784
constraints: self.constraints.build(),
785+
narrowing_constraints: self.narrowing_constraints.build(),
766786
visibility_constraints: self.visibility_constraints.build(),
767787
bindings_by_use: self.bindings_by_use,
768788
public_symbols: self.symbol_states,

0 commit comments

Comments
 (0)