Skip to content

Commit 79b9211

Browse files
authored
[red-knot] Further optimize *-import visibility constraints (#17375)
1 parent 1f85e0d commit 79b9211

File tree

2 files changed

+60
-56
lines changed

2 files changed

+60
-56
lines changed

crates/red_knot_python_semantic/src/semantic_index/builder.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,32 +1258,21 @@ where
12581258
symbol_id,
12591259
referenced_module,
12601260
);
1261-
let pre_definition = self.flow_snapshot();
1262-
self.push_additional_definition(symbol_id, node_ref);
12631261

12641262
// Fast path for if there were no previous definitions
12651263
// of the symbol defined through the `*` import:
12661264
// we can apply the visibility constraint to *only* the added definition,
12671265
// rather than all definitions
12681266
if newly_added {
1269-
let constraint_id = self
1270-
.current_use_def_map_mut()
1271-
.record_star_import_visibility_constraint(
1272-
star_import,
1273-
symbol_id,
1274-
);
1275-
1276-
let post_definition = self.flow_snapshot();
1277-
self.flow_restore(pre_definition);
1278-
1267+
self.push_additional_definition(symbol_id, node_ref);
12791268
self.current_use_def_map_mut()
1280-
.negate_star_import_visibility_constraint(
1269+
.record_and_negate_star_import_visibility_constraint(
1270+
star_import,
12811271
symbol_id,
1282-
constraint_id,
12831272
);
1284-
1285-
self.flow_merge(post_definition);
12861273
} else {
1274+
let pre_definition = self.flow_snapshot();
1275+
self.push_additional_definition(symbol_id, node_ref);
12871276
let constraint_id =
12881277
self.record_visibility_constraint(star_import.into());
12891278
let post_definition = self.flow_snapshot();

crates/red_knot_python_semantic/src/semantic_index/use_def.rs

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -766,32 +766,68 @@ impl<'db> UseDefMapBuilder<'db> {
766766
.add_and_constraint(self.scope_start_visibility, constraint);
767767
}
768768

769-
#[must_use = "A `*`-import visibility constraint must always be negated after it is added"]
770-
pub(super) fn record_star_import_visibility_constraint(
769+
/// This method exists solely as a fast path for handling `*`-import visibility constraints.
770+
///
771+
/// The reason why we add visibility constraints for [`Definition`]s created by `*` imports
772+
/// is laid out in the doc-comment for [`StarImportPlaceholderPredicate`]. But treating these
773+
/// visibility constraints in the use-def map the same way as all other visibility constraints
774+
/// was shown to lead to [significant regressions] for small codebases where typeshed
775+
/// dominates. (Although `*` imports are not common generally, they are used in several
776+
/// important places by typeshed.)
777+
///
778+
/// To solve these regressions, it was observed that we could add a fast path for `*`-import
779+
/// definitions which added a new symbol to the global scope (as opposed to `*`-import definitions
780+
/// that provided redefinitions for *pre-existing* global-scope symbols). The fast path does a
781+
/// number of things differently to our normal handling of visibility constraints:
782+
///
783+
/// - It only applies and negates the visibility constraints to a single symbol, rather than to
784+
/// all symbols. This is possible here because, unlike most definitions, we know in advance that
785+
/// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly
786+
/// which definition it is.
787+
///
788+
/// Doing things this way is cheaper in and of itself. However, it also allows us to avoid
789+
/// calling [`Self::simplify_visibility_constraints`] after the constraint has been applied to
790+
/// the "if-predicate-true" branch and negated for the "if-predicate-false" branch. Simplifying
791+
/// the visibility constraints is only important for symbols that did not have any new
792+
/// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch.
793+
///
794+
/// - It avoids multiple expensive calls to [`Self::snapshot`]. This is possible because we know
795+
/// the symbol is newly added, so we know the prior state of the symbol was
796+
/// [`SymbolState::undefined`].
797+
///
798+
/// - Normally we take care to check whether an "if-predicate-true" branch or an
799+
/// "if-predicate-false" branch contains a terminal statement: these can affect the visibility
800+
/// of symbols defined inside either branch. However, in the case of `*`-import definitions,
801+
/// this is unnecessary (and therefore not done in this method), since we know that a `*`-import
802+
/// predicate cannot create a terminal statement inside either branch.
803+
///
804+
/// [significant regressions]: https://github.com/astral-sh/ruff/pull/17286#issuecomment-2786755746
805+
pub(super) fn record_and_negate_star_import_visibility_constraint(
771806
&mut self,
772807
star_import: StarImportPlaceholderPredicate<'db>,
773808
symbol: ScopedSymbolId,
774-
) -> StarImportVisibilityConstraintId {
809+
) {
775810
let predicate_id = self.add_predicate(star_import.into());
776811
let visibility_id = self.visibility_constraints.add_atom(predicate_id);
777-
self.symbol_states[symbol]
812+
let negated_visibility_id = self
813+
.visibility_constraints
814+
.add_not_constraint(visibility_id);
815+
816+
let mut post_definition_state = std::mem::replace(
817+
&mut self.symbol_states[symbol],
818+
SymbolState::undefined(self.scope_start_visibility),
819+
);
820+
post_definition_state
778821
.record_visibility_constraint(&mut self.visibility_constraints, visibility_id);
779-
StarImportVisibilityConstraintId(visibility_id)
780-
}
781822

782-
pub(super) fn negate_star_import_visibility_constraint(
783-
&mut self,
784-
symbol_id: ScopedSymbolId,
785-
constraint: StarImportVisibilityConstraintId,
786-
) {
787-
let negated_constraint = self
788-
.visibility_constraints
789-
.add_not_constraint(constraint.into_scoped_constraint_id());
790-
self.symbol_states[symbol_id]
791-
.record_visibility_constraint(&mut self.visibility_constraints, negated_constraint);
792-
self.scope_start_visibility = self
793-
.visibility_constraints
794-
.add_and_constraint(self.scope_start_visibility, negated_constraint);
823+
self.symbol_states[symbol]
824+
.record_visibility_constraint(&mut self.visibility_constraints, negated_visibility_id);
825+
826+
self.symbol_states[symbol].merge(
827+
post_definition_state,
828+
&mut self.narrowing_constraints,
829+
&mut self.visibility_constraints,
830+
);
795831
}
796832

797833
/// This method resets the visibility constraints for all symbols to a previous state
@@ -1042,24 +1078,3 @@ impl<'db> UseDefMapBuilder<'db> {
10421078
}
10431079
}
10441080
}
1045-
1046-
/// Newtype wrapper over [`ScopedVisibilityConstraintId`] to improve type safety.
1047-
///
1048-
/// By returning this type from [`UseDefMapBuilder::record_star_import_visibility_constraint`]
1049-
/// rather than [`ScopedVisibilityConstraintId`] directly, we ensure that
1050-
/// [`UseDefMapBuilder::negate_star_import_visibility_constraint`] must be called after the
1051-
/// visibility constraint has been added, and we ensure that
1052-
/// [`super::SemanticIndexBuilder::record_negated_visibility_constraint`] *cannot* be called with
1053-
/// the narrowing constraint (which would lead to incorrect behaviour).
1054-
///
1055-
/// This type is defined here rather than in the [`super::visibility_constraints`] module
1056-
/// because it should only ever be constructed and deconstructed from methods in the
1057-
/// [`UseDefMapBuilder`].
1058-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1059-
pub(super) struct StarImportVisibilityConstraintId(ScopedVisibilityConstraintId);
1060-
1061-
impl StarImportVisibilityConstraintId {
1062-
fn into_scoped_constraint_id(self) -> ScopedVisibilityConstraintId {
1063-
self.0
1064-
}
1065-
}

0 commit comments

Comments
 (0)