[ty] Reachability constraints (#18621)

## Summary



* Completely removes the concept of visibility constraints. Reachability
constraints are now used to model the static visibility of bindings and
declarations. Reachability constraints are *much* easier to reason about
/ work with, since they are applied at the beginning of a branch, and
not applied retroactively. Removing the duplication between visibility
and reachability constraints also leads to major code simplifications
[^1]. For an overview of how the new constraint system works, see the
updated doc comment in `reachability_constraints.rs`.
* Fixes a [control-flow modeling bug
(panic)](https://github.com/astral-sh/ty/issues/365) involving `break`
statements in loops
* Fixes a [bug where](https://github.com/astral-sh/ty/issues/624) where
`elif` branches would have wrong reachability constraints
* Fixes a [bug where](https://github.com/astral-sh/ty/issues/648) code
after infinite loops would not be considered unreachble
* Fixes a panic on the `pywin32` ecosystem project, which we should be
able to move to `good.txt` once this has been merged.
* Removes some false positives in unreachable code because we infer
`Never` more often, due to the fact that reachability constraints now
apply retroactively to *all* active bindings, not just to bindings
inside a branch.
* As one example, this removes the `division-by-zero` diagnostic from
https://github.com/astral-sh/ty/issues/443 because we now infer `Never`
for the divisor.
* Supersedes and includes similar test changes as
https://github.com/astral-sh/ruff/pull/18392


closes https://github.com/astral-sh/ty/issues/365
closes https://github.com/astral-sh/ty/issues/624
closes https://github.com/astral-sh/ty/issues/642
closes https://github.com/astral-sh/ty/issues/648

## Benchmarks

Benchmarks on black, pandas, and sympy showed that this is neither a
performance improvement, nor a regression.

## Test Plan

Regression tests for:
- [x] https://github.com/astral-sh/ty/issues/365
- [x] https://github.com/astral-sh/ty/issues/624
- [x] https://github.com/astral-sh/ty/issues/642
- [x] https://github.com/astral-sh/ty/issues/648

[^1]: I'm afraid this is something that @carljm advocated for since the
beginning, and I'm not sure anymore why we have never seriously tried
this before. So I suggest we do *not* attempt to do a historical deep
dive to find out exactly why this ever became so complicated, and just
enjoy the fact that we eventually arrived here.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
David Peter 2025-06-17 09:24:28 +02:00 committed by GitHub
parent c22f809049
commit 3a77768f79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 683 additions and 806 deletions

View file

@ -755,65 +755,63 @@ fn place_from_bindings_impl<'db>(
requires_explicit_reexport: RequiresExplicitReExport,
) -> Place<'db> {
let predicates = bindings_with_constraints.predicates;
let visibility_constraints = bindings_with_constraints.visibility_constraints;
let reachability_constraints = bindings_with_constraints.reachability_constraints;
let mut bindings_with_constraints = bindings_with_constraints.peekable();
let is_non_exported = |binding: Definition<'db>| {
requires_explicit_reexport.is_yes() && !is_reexported(db, binding)
};
let unbound_visibility_constraint = match bindings_with_constraints.peek() {
let unbound_reachability_constraint = match bindings_with_constraints.peek() {
Some(BindingWithConstraints {
binding,
visibility_constraint,
reachability_constraint,
narrowing_constraint: _,
}) if binding.is_undefined_or(is_non_exported) => Some(*visibility_constraint),
}) if binding.is_undefined_or(is_non_exported) => Some(*reachability_constraint),
_ => None,
};
let mut deleted_visibility = Truthiness::AlwaysFalse;
let mut deleted_reachability = Truthiness::AlwaysFalse;
// Evaluate this lazily because we don't always need it (for example, if there are no visible
// bindings at all, we don't need it), and it can cause us to evaluate visibility constraint
// bindings at all, we don't need it), and it can cause us to evaluate reachability constraint
// expressions, which is extra work and can lead to cycles.
let unbound_visibility = || {
unbound_visibility_constraint
.map(|visibility_constraint| {
visibility_constraints.evaluate(db, predicates, visibility_constraint)
})
.unwrap_or(Truthiness::AlwaysFalse)
let unbound_reachability = || {
unbound_reachability_constraint.map(|reachability_constraint| {
reachability_constraints.evaluate(db, predicates, reachability_constraint)
})
};
let mut types = bindings_with_constraints.filter_map(
|BindingWithConstraints {
binding,
narrowing_constraint,
visibility_constraint,
reachability_constraint,
}| {
let binding =
match binding {
DefinitionState::Defined(binding) => binding,
DefinitionState::Undefined => {
return None;
}
DefinitionState::Deleted => {
deleted_visibility = deleted_visibility.or(
visibility_constraints.evaluate(db, predicates, visibility_constraint)
);
return None;
}
};
let binding = match binding {
DefinitionState::Defined(binding) => binding,
DefinitionState::Undefined => {
return None;
}
DefinitionState::Deleted => {
deleted_reachability = deleted_reachability.or(
reachability_constraints.evaluate(db, predicates, reachability_constraint)
);
return None;
}
};
if is_non_exported(binding) {
return None;
}
let static_visibility =
visibility_constraints.evaluate(db, predicates, visibility_constraint);
let static_reachability =
reachability_constraints.evaluate(db, predicates, reachability_constraint);
if static_visibility.is_always_false() {
// We found a binding that we have statically determined to not be visible from
// the use of the place that we are investigating. There are three interesting
// cases to consider:
if static_reachability.is_always_false() {
// If the static reachability evaluates to false, the binding is either not reachable
// from the start of the scope, or there is no control flow path from that binding to
// the use of the place that we are investigating. There are three interesting cases
// to consider:
//
// ```py
// def f1():
@ -827,35 +825,37 @@ fn place_from_bindings_impl<'db>(
// use(y)
//
// def f3(flag: bool):
// z = 1
// if flag:
// z = 1
// else:
// z = 2
// return
// use(z)
// ```
//
// In the first case, there is a single binding for `x`, and due to the statically
// known `False` condition, it is not visible at the use of `x`. However, we *can*
// see/reach the start of the scope from `use(x)`. This means that `x` is unbound
// and we should return `None`.
// In the first case, there is a single binding for `x`, but it is not reachable from
// the start of the scope. However, the use of `x` is reachable (`unbound_reachability`
// is not always-false). This means that `x` is unbound and we should return `None`.
//
// In the second case, `y` is also not visible at the use of `y`, but here, we can
// not see/reach the start of the scope. There is only one path of control flow,
// and it passes through that binding of `y` (which we can not see). This implies
// that we are in an unreachable section of code. We return `Never` in order to
// silence the `unresolve-reference` diagnostic that would otherwise be emitted at
// the use of `y`.
// In the second case, the binding of `y` is reachable, but there is no control flow
// path from the beginning of the scope, through that binding, to the use of `y` that
// we are investigating. There is also no control flow path from the start of the
// scope, through the implicit `y = <unbound>` binding, to the use of `y`. This means
// that `unbound_reachability` is always false. Since there are no other bindings, no
// control flow path can reach this use of `y`, implying that we are in unreachable
// section of code. We return `Never` in order to silence the `unresolve-reference`
// diagnostic that would otherwise be emitted at the use of `y`.
//
// In the third case, we have two bindings for `z`. The first one is visible, so we
// consider the case that we now encounter the second binding `z = 2`, which is not
// visible due to the early return. We *also* can not see the start of the scope
// from `use(z)` because both paths of control flow pass through a binding of `z`.
// The `z = 1` binding is visible, and so we are *not* in an unreachable section of
// code. However, it is still okay to return `Never` in this case, because we will
// union the types of all bindings, and `Never` will be eliminated automatically.
// In the third case, we have two bindings for `z`. The first one is visible (there
// is a path of control flow from the start of the scope, through that binding, to
// the use of `z`). So we consider the case that we now encounter the second binding
// `z = 2`, which is not visible due to the early return. The `z = <unbound>` binding
// is not live (shadowed by the other bindings), so `unbound_reachability` is `None`.
// Here, we are *not* in an unreachable section of code. However, it is still okay to
// return `Never` in this case, because we will union the types of all bindings, and
// `Never` will be eliminated automatically.
if unbound_visibility().is_always_false() {
// The scope-start is not visible
if unbound_reachability().is_none_or(Truthiness::is_always_false) {
return Some(Type::Never);
}
return None;
@ -867,14 +867,14 @@ fn place_from_bindings_impl<'db>(
);
if let Some(first) = types.next() {
let boundness = match unbound_visibility() {
Truthiness::AlwaysTrue => {
let boundness = match unbound_reachability() {
Some(Truthiness::AlwaysTrue) => {
unreachable!(
"If we have at least one binding, the scope-start should not be definitely visible"
"If we have at least one binding, the implicit `unbound` binding should not be definitely visible"
)
}
Truthiness::AlwaysFalse => Boundness::Bound,
Truthiness::Ambiguous => Boundness::PossiblyUnbound,
Some(Truthiness::AlwaysFalse) | None => Boundness::Bound,
Some(Truthiness::Ambiguous) => Boundness::PossiblyUnbound,
};
let ty = if let Some(second) = types.next() {
@ -882,7 +882,7 @@ fn place_from_bindings_impl<'db>(
} else {
first
};
match deleted_visibility {
match deleted_reachability {
Truthiness::AlwaysFalse => Place::Type(ty, boundness),
Truthiness::AlwaysTrue => Place::Unbound,
Truthiness::Ambiguous => Place::Type(ty, Boundness::PossiblyUnbound),
@ -903,19 +903,19 @@ fn place_from_declarations_impl<'db>(
requires_explicit_reexport: RequiresExplicitReExport,
) -> PlaceFromDeclarationsResult<'db> {
let predicates = declarations.predicates;
let visibility_constraints = declarations.visibility_constraints;
let reachability_constraints = declarations.reachability_constraints;
let mut declarations = declarations.peekable();
let is_non_exported = |declaration: Definition<'db>| {
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
};
let undeclared_visibility = match declarations.peek() {
let undeclared_reachability = match declarations.peek() {
Some(DeclarationWithConstraint {
declaration,
visibility_constraint,
reachability_constraint,
}) if declaration.is_undefined_or(is_non_exported) => {
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
reachability_constraints.evaluate(db, predicates, *reachability_constraint)
}
_ => Truthiness::AlwaysFalse,
};
@ -923,7 +923,7 @@ fn place_from_declarations_impl<'db>(
let mut types = declarations.filter_map(
|DeclarationWithConstraint {
declaration,
visibility_constraint,
reachability_constraint,
}| {
let DefinitionState::Defined(declaration) = declaration else {
return None;
@ -933,10 +933,10 @@ fn place_from_declarations_impl<'db>(
return None;
}
let static_visibility =
visibility_constraints.evaluate(db, predicates, visibility_constraint);
let static_reachability =
reachability_constraints.evaluate(db, predicates, reachability_constraint);
if static_visibility.is_always_false() {
if static_reachability.is_always_false() {
None
} else {
Some(declaration_type(db, declaration))
@ -964,10 +964,10 @@ fn place_from_declarations_impl<'db>(
first
};
if conflicting.is_empty() {
let boundness = match undeclared_visibility {
let boundness = match undeclared_reachability {
Truthiness::AlwaysTrue => {
unreachable!(
"If we have at least one declaration, the scope-start should not be definitely visible"
"If we have at least one declaration, the implicit `unbound` binding should not be definitely visible"
)
}
Truthiness::AlwaysFalse => Boundness::Bound,

View file

@ -33,8 +33,8 @@ pub(crate) mod narrowing_constraints;
pub mod place;
pub(crate) mod predicate;
mod re_exports;
mod reachability_constraints;
mod use_def;
mod visibility_constraints;
pub(crate) use self::use_def::{
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,

View file

@ -40,12 +40,12 @@ use crate::semantic_index::predicate::{
StarImportPlaceholderPredicate,
};
use crate::semantic_index::re_exports::exported_names;
use crate::semantic_index::reachability_constraints::{
ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId,
};
use crate::semantic_index::use_def::{
EagerSnapshotKey, FlowSnapshot, ScopedEagerSnapshotId, UseDefMapBuilder,
};
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
};
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::{Db, Program};
@ -157,7 +157,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
builder.push_scope_with_parent(
NodeWithScopeRef::Module,
None,
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
builder
@ -237,7 +237,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
&mut self,
node: NodeWithScopeRef,
parent: Option<FileScopeId>,
reachability: ScopedVisibilityConstraintId,
reachability: ScopedReachabilityConstraintId,
) {
let children_start = self.scopes.next_index() + 1;
@ -354,9 +354,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
&self.use_def_maps[scope_id]
}
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder {
fn current_reachability_constraints_mut(&mut self) -> &mut ReachabilityConstraintsBuilder {
let scope_id = self.current_scope();
&mut self.use_def_maps[scope_id].visibility_constraints
&mut self.use_def_maps[scope_id].reachability_constraints
}
fn current_ast_ids(&mut self) -> &mut AstIdsBuilder {
@ -576,55 +576,15 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
id
}
/// Records a previously added visibility constraint by applying it to all live bindings
/// and declarations.
fn record_visibility_constraint_id(&mut self, constraint: ScopedVisibilityConstraintId) {
self.current_use_def_map_mut()
.record_visibility_constraint(constraint);
}
/// Negates the given visibility constraint and then adds it to all live bindings and declarations.
fn record_negated_visibility_constraint(
&mut self,
constraint: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
let id = self
.current_visibility_constraints_mut()
.add_not_constraint(constraint);
self.record_visibility_constraint_id(id);
id
}
/// Records a visibility constraint by applying it to all live bindings and declarations.
fn record_visibility_constraint(
&mut self,
predicate: Predicate<'db>,
) -> ScopedVisibilityConstraintId {
let predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
let id = self
.current_visibility_constraints_mut()
.add_atom(predicate_id);
self.record_visibility_constraint_id(id);
id
}
/// Records that all remaining statements in the current block are unreachable, and therefore
/// not visible.
/// Records that all remaining statements in the current block are unreachable.
fn mark_unreachable(&mut self) {
self.current_use_def_map_mut().mark_unreachable();
}
/// Records a visibility constraint that always evaluates to "ambiguous".
fn record_ambiguous_visibility(&mut self) {
/// Records a reachability constraint that always evaluates to "ambiguous".
fn record_ambiguous_reachability(&mut self) {
self.current_use_def_map_mut()
.record_visibility_constraint(ScopedVisibilityConstraintId::AMBIGUOUS);
}
/// Simplifies (resets) visibility constraints on all live bindings and declarations that did
/// not see any new definitions since the given snapshot.
fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
self.current_use_def_map_mut()
.simplify_visibility_constraints(snapshot);
.record_reachability_constraint(ScopedReachabilityConstraintId::AMBIGUOUS);
}
/// Record a constraint that affects the reachability of the current position in the semantic
@ -634,7 +594,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
fn record_reachability_constraint(
&mut self,
predicate: Predicate<'db>,
) -> ScopedVisibilityConstraintId {
) -> ScopedReachabilityConstraintId {
let predicate_id = self.add_predicate(predicate);
self.record_reachability_constraint_id(predicate_id)
}
@ -643,22 +603,22 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
fn record_reachability_constraint_id(
&mut self,
predicate_id: ScopedPredicateId,
) -> ScopedVisibilityConstraintId {
let visibility_constraint = self
.current_visibility_constraints_mut()
) -> ScopedReachabilityConstraintId {
let reachability_constraint = self
.current_reachability_constraints_mut()
.add_atom(predicate_id);
self.current_use_def_map_mut()
.record_reachability_constraint(visibility_constraint);
visibility_constraint
.record_reachability_constraint(reachability_constraint);
reachability_constraint
}
/// Record the negation of a given reachability/visibility constraint.
/// Record the negation of a given reachability constraint.
fn record_negated_reachability_constraint(
&mut self,
reachability_constraint: ScopedVisibilityConstraintId,
reachability_constraint: ScopedReachabilityConstraintId,
) {
let negated_constraint = self
.current_visibility_constraints_mut()
.current_reachability_constraints_mut()
.add_not_constraint(reachability_constraint);
self.current_use_def_map_mut()
.record_reachability_constraint(negated_constraint);
@ -1139,12 +1099,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_))
.then(|| builder.flow_snapshot());
builder.visit_stmt(last_stmt);
let scope_start_visibility =
builder.current_use_def_map().scope_start_visibility;
let reachability = builder.current_use_def_map().reachability;
if let Some(pre_return_state) = pre_return_state {
builder.flow_restore(pre_return_state);
builder.current_use_def_map_mut().scope_start_visibility =
scope_start_visibility;
builder.current_use_def_map_mut().reachability = reachability;
}
}
@ -1297,11 +1255,11 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
continue;
};
// In order to understand the visibility of definitions created by a `*` import,
// we need to know the visibility of the global-scope definitions in the
// In order to understand the reachability of definitions created by a `*` import,
// we need to know the reachability of the global-scope definitions in the
// `referenced_module` the symbols imported from. Much like predicates for `if`
// statements can only have their visibility constraints resolved at type-inference
// time, the visibility of these global-scope definitions in the external module
// statements can only have their reachability constraints resolved at type-inference
// time, the reachability of these global-scope definitions in the external module
// cannot be resolved at this point. As such, we essentially model each definition
// stemming from a `from exporter *` import as something like:
//
@ -1328,7 +1286,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
self.current_use_def_map().single_place_snapshot(symbol_id);
self.push_additional_definition(symbol_id, node_ref);
self.current_use_def_map_mut()
.record_and_negate_star_import_visibility_constraint(
.record_and_negate_star_import_reachability_constraint(
star_import,
symbol_id,
pre_definition,
@ -1387,7 +1345,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
// reachability constraint on the `msg` expression.
//
// The other important part is the `<halt>`. This lets us skip the usual merging of
// flow states and simplification of visibility constraints, since there is no way
// flow states and simplification of reachability constraints, since there is no way
// of getting out of that `msg` branch. We simply restore to the post-test state.
self.visit_expr(test);
@ -1399,12 +1357,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
self.record_narrowing_constraint(negated_predicate);
self.record_reachability_constraint(negated_predicate);
self.visit_expr(msg);
self.record_visibility_constraint(negated_predicate);
self.flow_restore(post_test);
}
self.record_narrowing_constraint(predicate);
self.record_visibility_constraint(predicate);
self.record_reachability_constraint(predicate);
}
@ -1479,13 +1435,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
self.visit_expr(&node.test);
let mut no_branch_taken = self.flow_snapshot();
let mut last_predicate = self.record_expression_narrowing_constraint(&node.test);
let mut reachability_constraint =
let mut last_reachability_constraint =
self.record_reachability_constraint(last_predicate);
self.visit_body(&node.body);
let visibility_constraint_id = self.record_visibility_constraint(last_predicate);
let mut vis_constraints = vec![visibility_constraint_id];
let mut post_clauses: Vec<FlowSnapshot> = vec![];
let elif_else_clauses = node
.elif_else_clauses
@ -1509,38 +1462,27 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
// we can only take an elif/else branch if none of the previous ones were
// taken
self.flow_restore(no_branch_taken.clone());
self.record_negated_narrowing_constraint(last_predicate);
self.record_negated_reachability_constraint(reachability_constraint);
let elif_predicate = if let Some(elif_test) = clause_test {
self.record_negated_narrowing_constraint(last_predicate);
self.record_negated_reachability_constraint(last_reachability_constraint);
if let Some(elif_test) = clause_test {
self.visit_expr(elif_test);
// A test expression is evaluated whether the branch is taken or not
no_branch_taken = self.flow_snapshot();
reachability_constraint =
last_predicate = self.record_expression_narrowing_constraint(elif_test);
last_reachability_constraint =
self.record_reachability_constraint(last_predicate);
let predicate = self.record_expression_narrowing_constraint(elif_test);
Some(predicate)
} else {
None
};
}
self.visit_body(clause_body);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
}
if let Some(elif_predicate) = elif_predicate {
last_predicate = elif_predicate;
let id = self.record_visibility_constraint(elif_predicate);
vis_constraints.push(id);
}
}
for post_clause_state in post_clauses {
self.flow_merge(post_clause_state);
}
self.simplify_visibility_constraints(no_branch_taken);
}
ast::Stmt::While(ast::StmtWhile {
test,
@ -1555,57 +1497,49 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
let predicate = self.record_expression_narrowing_constraint(test);
self.record_reachability_constraint(predicate);
// We need multiple copies of the visibility constraint for the while condition,
// We need multiple copies of the reachability constraint for the while condition,
// since we need to model situations where the first evaluation of the condition
// returns True, but a later evaluation returns False.
let first_predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
let later_predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
let first_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(first_predicate_id);
let later_vis_constraint_id = self
.current_visibility_constraints_mut()
.current_reachability_constraints_mut()
.add_atom(later_predicate_id);
// If the body is executed, we know that we've evaluated the condition at least
// once, and that the first evaluation was True. We might not have evaluated the
// condition more than once, so we can't assume that later evaluations were True.
// So the body's full reachability constraint is `first`.
self.record_reachability_constraint_id(first_predicate_id);
let outer_loop = self.push_loop();
self.visit_body(body);
let this_loop = self.pop_loop(outer_loop);
// If the body is executed, we know that we've evaluated the condition at least
// once, and that the first evaluation was True. We might not have evaluated the
// condition more than once, so we can't assume that later evaluations were True.
// So the body's full visibility constraint is `first`.
let body_vis_constraint_id = first_vis_constraint_id;
self.record_visibility_constraint_id(body_vis_constraint_id);
// We execute the `else` once the condition evaluates to false. This could happen
// without ever executing the body, if the condition is false the first time it's
// tested. So the starting flow state of the `else` clause is the union of:
// - the pre-loop state with a visibility constraint that the first evaluation of
// - the pre-loop state with a reachability constraint that the first evaluation of
// the while condition was false,
// - the post-body state (which already has a visibility constraint that the
// first evaluation was true) with a visibility constraint that a _later_
// - the post-body state (which already has a reachability constraint that the
// first evaluation was true) with a reachability constraint that a _later_
// evaluation of the while condition was false.
// To model this correctly, we need two copies of the while condition constraint,
// since the first and later evaluations might produce different results.
let post_body = self.flow_snapshot();
self.flow_restore(pre_loop.clone());
self.record_negated_visibility_constraint(first_vis_constraint_id);
self.flow_restore(pre_loop);
self.flow_merge(post_body);
self.record_negated_narrowing_constraint(predicate);
self.record_negated_reachability_constraint(later_vis_constraint_id);
self.visit_body(orelse);
self.record_negated_visibility_constraint(later_vis_constraint_id);
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in this_loop.break_states {
let snapshot = self.flow_snapshot();
self.flow_restore(break_state);
self.record_visibility_constraint_id(body_vis_constraint_id);
self.flow_merge(snapshot);
}
self.simplify_visibility_constraints(pre_loop);
}
ast::Stmt::With(ast::StmtWith {
items,
@ -1652,7 +1586,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
let iter_expr = self.add_standalone_expression(iter);
self.visit_expr(iter);
self.record_ambiguous_visibility();
self.record_ambiguous_reachability();
let pre_loop = self.flow_snapshot();
@ -1709,10 +1643,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
&case.pattern,
case.guard.as_deref(),
);
let vis_constraint_id = self.record_reachability_constraint(match_predicate);
let reachability_constraint =
self.record_reachability_constraint(match_predicate);
let match_success_guard_failure = case.guard.as_ref().map(|guard| {
let guard_expr = self.add_standalone_expression(guard);
// We could also add the guard expression as a reachability constraint, but
// it seems unlikely that both the case predicate as well as the guard are
// statically known conditions, so we currently don't model that.
self.record_ambiguous_reachability();
self.visit_expr(guard);
let post_guard_eval = self.flow_snapshot();
let predicate = Predicate {
@ -1726,8 +1665,6 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
match_success_guard_failure
});
self.record_visibility_constraint_id(vis_constraint_id);
self.visit_body(&case.body);
post_case_snapshots.push(self.flow_snapshot());
@ -1738,6 +1675,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
// snapshots into.
self.flow_restore(no_case_matched.clone());
self.record_negated_narrowing_constraint(match_predicate);
self.record_negated_reachability_constraint(reachability_constraint);
if let Some(match_success_guard_failure) = match_success_guard_failure {
self.flow_merge(match_success_guard_failure);
} else {
@ -1748,15 +1686,12 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
debug_assert!(case.guard.is_none());
}
self.record_negated_visibility_constraint(vis_constraint_id);
no_case_matched = self.flow_snapshot();
}
for post_clause_state in post_case_snapshots {
self.flow_merge(post_clause_state);
}
self.simplify_visibility_constraints(no_case_matched);
}
ast::Stmt::Try(ast::StmtTry {
body,
@ -1767,7 +1702,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
range: _,
node_index: _,
}) => {
self.record_ambiguous_visibility();
self.record_ambiguous_reachability();
// Save the state prior to visiting any of the `try` block.
//
@ -2136,16 +2071,13 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
let predicate = self.record_expression_narrowing_constraint(test);
let reachability_constraint = self.record_reachability_constraint(predicate);
self.visit_expr(body);
let visibility_constraint = self.record_visibility_constraint(predicate);
let post_body = self.flow_snapshot();
self.flow_restore(pre_if.clone());
self.flow_restore(pre_if);
self.record_negated_narrowing_constraint(predicate);
self.record_negated_reachability_constraint(reachability_constraint);
self.visit_expr(orelse);
self.record_negated_visibility_constraint(visibility_constraint);
self.flow_merge(post_body);
self.simplify_visibility_constraints(pre_if);
}
ast::Expr::ListComp(
list_comprehension @ ast::ExprListComp {
@ -2203,18 +2135,17 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
node_index: _,
op,
}) => {
let pre_op = self.flow_snapshot();
let mut snapshots = vec![];
let mut visibility_constraints = vec![];
let mut reachability_constraints = vec![];
for (index, value) in values.iter().enumerate() {
self.visit_expr(value);
for vid in &visibility_constraints {
self.record_visibility_constraint_id(*vid);
for id in &reachability_constraints {
self.current_use_def_map_mut()
.record_reachability_constraint(*id); // TODO: nicer API
}
self.visit_expr(value);
// For the last value, we don't need to model control flow. There is no short-circuiting
// anymore.
if index < values.len() - 1 {
@ -2223,37 +2154,32 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
ast::BoolOp::And => self.add_predicate(predicate),
ast::BoolOp::Or => self.add_negated_predicate(predicate),
};
let visibility_constraint = self
.current_visibility_constraints_mut()
let reachability_constraint = self
.current_reachability_constraints_mut()
.add_atom(predicate_id);
let after_expr = self.flow_snapshot();
// We first model the short-circuiting behavior. We take the short-circuit
// path here if all of the previous short-circuit paths were not taken, so
// we record all previously existing visibility constraints, and negate the
// we record all previously existing reachability constraints, and negate the
// one for the current expression.
for vid in &visibility_constraints {
self.record_visibility_constraint_id(*vid);
}
self.record_negated_visibility_constraint(visibility_constraint);
self.record_negated_reachability_constraint(reachability_constraint);
snapshots.push(self.flow_snapshot());
// Then we model the non-short-circuiting behavior. Here, we need to delay
// the application of the visibility constraint until after the expression
// the application of the reachability constraint until after the expression
// has been evaluated, so we only push it onto the stack here.
self.flow_restore(after_expr);
self.record_narrowing_constraint_id(predicate_id);
self.record_reachability_constraint_id(predicate_id);
visibility_constraints.push(visibility_constraint);
reachability_constraints.push(reachability_constraint);
}
}
for snapshot in snapshots {
self.flow_merge(snapshot);
}
self.simplify_visibility_constraints(pre_op);
}
ast::Expr::StringLiteral(_) => {
// Track reachability of string literals, as they could be a stringified annotation

View file

@ -15,7 +15,7 @@ use smallvec::{SmallVec, smallvec};
use crate::Db;
use crate::ast_node_ref::AstNodeRef;
use crate::node_key::NodeKey;
use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId;
use crate::semantic_index::reachability_constraints::ScopedReachabilityConstraintId;
use crate::semantic_index::{PlaceSet, SemanticIndex, semantic_index};
#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)]
@ -480,7 +480,7 @@ pub struct Scope {
parent: Option<FileScopeId>,
node: NodeWithScopeKind,
descendants: Range<FileScopeId>,
reachability: ScopedVisibilityConstraintId,
reachability: ScopedReachabilityConstraintId,
}
impl Scope {
@ -488,7 +488,7 @@ impl Scope {
parent: Option<FileScopeId>,
node: NodeWithScopeKind,
descendants: Range<FileScopeId>,
reachability: ScopedVisibilityConstraintId,
reachability: ScopedReachabilityConstraintId,
) -> Self {
Scope {
parent,
@ -522,7 +522,7 @@ impl Scope {
self.kind().is_eager()
}
pub(crate) fn reachability(&self) -> ScopedVisibilityConstraintId {
pub(crate) fn reachability(&self) -> ScopedReachabilityConstraintId {
self.reachability
}
}

View file

@ -4,8 +4,8 @@
//!
//! - [_Narrowing constraints_][crate::semantic_index::narrowing_constraints] constrain the type of
//! a binding that is visible at a particular use.
//! - [_Visibility constraints_][crate::semantic_index::visibility_constraints] determine the
//! static visibility of a binding, and the reachability of a statement.
//! - [_Reachability constraints_][crate::semantic_index::reachability_constraints] determine the
//! static reachability of a binding, and the reachability of a statement or expression.
use ruff_db::files::File;
use ruff_index::{IndexVec, newtype_index};
@ -65,7 +65,7 @@ pub(crate) enum PredicateNode<'db> {
StarImportPlaceholder(StarImportPlaceholderPredicate<'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 reachability analysis.
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
pub(crate) enum PatternPredicateKind<'db> {
Singleton(Singleton),
@ -99,7 +99,7 @@ impl<'db> PatternPredicate<'db> {
/// A "placeholder predicate" that is used to model the fact that the boundness of a
/// (possible) definition or declaration caused by a `*` import cannot be fully determined
/// until type-inference time. This is essentially the same as a standard visibility constraint,
/// until type-inference time. This is essentially the same as a standard reachability constraint,
/// so we reuse the [`Predicate`] infrastructure to model it.
///
/// To illustrate, say we have a module `exporter.py` like so:

View file

@ -1,61 +1,37 @@
//! # Visibility constraints
//! # Reachability constraints
//!
//! During semantic index building, we collect visibility constraints for each binding and
//! declaration. These constraints are then used during type-checking to determine the static
//! visibility of a certain definition. This allows us to re-analyze control flow during type
//! checking, potentially "hiding" some branches that we can statically determine to never be
//! taken. Consider the following example first. We added implicit "unbound" definitions at the
//! start of the scope. Note how visibility constraints can apply to bindings outside of the
//! if-statement:
//! During semantic index building, we record so-called reachability constraints that keep track
//! of a set of conditions that need to apply in order for a certain statement or expression to
//! be reachable from the start of the scope. As an example, consider the following situation where
//! we have just processed two `if`-statements:
//! ```py
//! x = <unbound> # not a live binding for the use of x below, shadowed by `x = 1`
//! y = <unbound> # visibility constraint: ~test
//!
//! x = 1 # visibility constraint: ~test
//! if test:
//! x = 2 # visibility constraint: test
//!
//! y = 2 # visibility constraint: test
//!
//! use(x)
//! use(y)
//! <is this reachable?>
//! ```
//! The static truthiness of the `test` condition can either be always-false, ambiguous, or
//! always-true. Similarly, we have the same three options when evaluating a visibility constraint.
//! This outcome determines the visibility of a definition: always-true means that the definition
//! is definitely visible for a given use, always-false means that the definition is definitely
//! not visible, and ambiguous means that we might see this definition or not. In the latter case,
//! we need to consider both options during type inference and boundness analysis. For the example
//! above, these are the possible type inference / boundness results for the uses of `x` and `y`:
//! In this case, we would record a reachability constraint of `test`, which would later allow us
//! to re-analyze the control flow during type-checking, once we actually know the static truthiness
//! of `test`. When evaluating a constraint, there are three possible outcomes: always true, always
//! false, or ambiguous. For a simple constraint like this, always-true and always-false correspond
//! to the case in which we can infer that the type of `test` is `Literal[True]` or `Literal[False]`.
//! In any other case, like if the type of `test` is `bool` or `Unknown`, we can not statically
//! determine whether `test` is truthy or falsy, so the outcome would be "ambiguous".
//!
//! ```text
//! | `test` truthiness | `~test` truthiness | type of `x` | boundness of `y` |
//! |-------------------|--------------------|-----------------|------------------|
//! | always false | always true | `Literal[1]` | unbound |
//! | ambiguous | ambiguous | `Literal[1, 2]` | possibly unbound |
//! | always true | always false | `Literal[2]` | bound |
//! ```
//!
//! ### Sequential constraints (ternary AND)
//! ## Sequential constraints (ternary AND)
//!
//! As we have seen above, visibility constraints can apply outside of a control flow element.
//! So we need to consider the possibility that multiple constraints apply to the same binding.
//! Here, we consider what happens if multiple `if`-statements lead to a sequence of constraints.
//! Consider the following example:
//! Whenever control flow branches, we record reachability constraints. If we already have a
//! constraint, we create a new one using a ternary AND operation. Consider the following example:
//! ```py
//! x = 0
//!
//! if test1:
//! x = 1
//!
//! if test2:
//! x = 2
//! if test2:
//! <is this reachable?>
//! ```
//! The binding `x = 2` is easy to analyze. Its visibility corresponds to the truthiness of `test2`.
//! For the `x = 1` binding, things are a bit more interesting. It is always visible if `test1` is
//! always-true *and* `test2` is always-false. It is never visible if `test1` is always-false *or*
//! `test2` is always-true. And it is ambiguous otherwise. This corresponds to a ternary *test1 AND
//! ~test2* operation in three-valued Kleene logic [Kleene]:
//! Here, we would accumulate a reachability constraint of `test1 AND test2`. We can statically
//! determine that this position is *always* reachable only if both `test1` and `test2` are
//! always true. On the other hand, we can statically determine that this position is *never*
//! reachable if *either* `test1` or `test2` is always false. In any other case, we can not
//! determine whether this position is reachable or not, so the outcome is "ambiguous". This
//! corresponds to a ternary *AND* operation in [Kleene] logic:
//!
//! ```text
//! | AND | always-false | ambiguous | always-true |
@ -65,38 +41,27 @@
//! | always true | always-false | ambiguous | always-true |
//! ```
//!
//! The `x = 0` binding can be handled similarly, with the difference that both `test1` and `test2`
//! are negated:
//!
//! ## Merged constraints (ternary OR)
//!
//! We also need to consider the case where control flow merges again. Consider a case like this:
//! ```py
//! x = 0 # ~test1 AND ~test2
//!
//! if test1:
//! x = 1 # test1 AND ~test2
//!
//! if test2:
//! x = 2 # test2
//! ```
//!
//! ### Merged constraints (ternary OR)
//!
//! Finally, we consider what happens in "parallel" control flow. Consider the following example
//! where we have omitted the test condition for the outer `if` for clarity:
//! ```py
//! x = 0
//!
//! if <…>:
//! def _():
//! if test1:
//! x = 1
//! else:
//! if test2:
//! x = 2
//! pass
//! elif test2:
//! pass
//! else:
//! return
//!
//! use(x)
//! <is this reachable?>
//! ```
//! At the usage of `x`, i.e. after control flow has been merged again, the visibility of the `x =
//! 0` binding behaves as follows: the binding is always visible if `test1` is always-false *or*
//! `test2` is always-false; and it is never visible if `test1` is always-true *and* `test2` is
//! always-true. This corresponds to a ternary *OR* operation in Kleene logic:
//! Here, the first branch has a `test1` constraint, and the second branch has a `test2` constraint.
//! The third branch ends in a terminal statement [^1]. When we merge control flow, we need to consider
//! the reachability through either the first or the second branch. The current position is only
//! *definitely* unreachable if both `test1` and `test2` are always false. It is definitely
//! reachable if *either* `test1` or `test2` is always true. In any other case, we can not statically
//! determine whether it is reachable or not. This operation corresponds to a ternary *OR* operation:
//!
//! ```text
//! | OR | always-false | ambiguous | always-true |
@ -106,40 +71,95 @@
//! | always true | always-true | always-true | always-true |
//! ```
//!
//! Using this, we can annotate the visibility constraints for the example above:
//! ```py
//! x = 0 # ~test1 OR ~test2
//! [^1]: What's actually happening here is that we merge all three branches using a ternary OR. The
//! third branch has a reachability constraint of `always-false`, and `t OR always-false` is equal
//! to `t` (see first column in that table), so it was okay to omit the third branch in the discussion
//! above.
//!
//! if <…>:
//! if test1:
//! x = 1 # test1
//!
//! ## Negation
//!
//! Control flow elements like `if-elif-else` or `match` statements can also lead to negated
//! constraints. For example, we record a constraint of `~test` for the `else` branch here:
//! ```py
//! if test:
//! pass
//! else:
//! if test2:
//! x = 2 # test2
//! <is this reachable?>
//! ```
//!
//! ## Explicit ambiguity
//!
//! In some cases, we explicitly record an “ambiguous” constraint. We do this when branching on
//! something that we can not (or intentionally do not want to) analyze statically. `for` loops are
//! one example:
//! ```py
//! def _():
//! for _ in range(2):
//! return
//!
//! <is this reachable?>
//! ```
//! If we would not record any constraints at the branching point, we would have an `always-true`
//! reachability for the no-loop branch, and a `always-false` reachability for the branch which enters
//! the loop. Merging those would lead to a reachability of `always-true OR always-false = always-true`,
//! i.e. we would consider the end of the scope to be unconditionally reachable, which is not correct.
//!
//! Recording an ambiguous constraint at the branching point modifies the constraints in both branches to
//! `always-true AND ambiguous = ambiguous` and `always-false AND ambiguous = always-false`, respectively.
//! Merging these two using OR correctly leads to `ambiguous` for the end-of-scope reachability.
//!
//!
//! ## Reachability constraints and bindings
//!
//! To understand how reachability constraints apply to bindings in particular, consider the following
//! example:
//! ```py
//! x = <unbound> # not a live binding for the use of x below, shadowed by `x = 1`
//! y = <unbound> # reachability constraint: ~test
//!
//! x = 1 # reachability constraint: ~test
//! if test:
//! x = 2 # reachability constraint: test
//!
//! y = 2 # reachability constraint: test
//!
//! use(x)
//! use(y)
//! ```
//! Both the type and the boundness of `x` and `y` are affected by reachability constraints:
//!
//! ```text
//! | `test` truthiness | type of `x` | boundness of `y` |
//! |-------------------|-----------------|------------------|
//! | always false | `Literal[1]` | unbound |
//! | ambiguous | `Literal[1, 2]` | possibly unbound |
//! | always true | `Literal[2]` | bound |
//! ```
//!
//! ### Explicit ambiguity
//! To achieve this, we apply reachability constraints retroactively to bindings that came before
//! the branching point. In the example above, the `x = 1` binding has a `test` constraint in the
//! `if` branch, and a `~test` constraint in the implicit `else` branch. Since it is shadowed by
//! `x = 2` in the `if` branch, we are only left with the `~test` constraint after control flow
//! has merged again.
//!
//! In some cases, we explicitly add an “ambiguous” constraint to all bindings
//! in a certain control flow path. We do this when branching on something that we can not (or
//! intentionally do not want to) analyze statically. `for` loops are one example:
//! ```py
//! x = <unbound>
//! For live bindings, the reachability constraint therefore refers to the following question:
//! Is the binding reachable from the start of the scope, and is there a control flow path from
//! that binding to a use of that symbol at the current position?
//!
//! for _ in range(2):
//! x = 1
//! ```
//! Here, we report an ambiguous visibility constraint before branching off. If we don't do this,
//! the `x = <unbound>` binding would be considered unconditionally visible in the no-loop case.
//! And since the other branch does not have the live `x = <unbound>` binding, we would incorrectly
//! create a state where the `x = <unbound>` binding is always visible.
//! In the example above, `x = 1` is always reachable, but that binding can only reach the use of
//! `x` at the current position if `test` is falsy.
//!
//! To handle boundness correctly, we also add implicit `y = <unbound>` bindings at the start of
//! the scope. This allows us to determine whether a symbol is definitely bound (if that implicit
//! `y = <unbound>` binding is not visible), possibly unbound (if the reachability constraint
//! evaluates to `Ambiguous`), or definitely unbound (in case the `y = <unbound>` binding is
//! always visible).
//!
//!
//! ### Representing formulas
//!
//! Given everything above, we can represent a visibility constraint as a _ternary formula_. This
//! Given everything above, we can represent a reachability constraint as a _ternary formula_. This
//! is like a boolean formula (which maps several true/false variables to a single true/false
//! result), but which allows the third "ambiguous" value in addition to "true" and "false".
//!
@ -166,7 +186,7 @@
//! formulas (which have the same outputs for every combination of inputs) are represented by
//! exactly the same graph node. (Because of interning, this is not _equal_ nodes, but _identical_
//! ones.) That means that we can compare formulas for equivalence in constant time, and in
//! particular, can check whether a visibility constraint is statically always true or false,
//! particular, can check whether a reachability constraint is statically always true or false,
//! regardless of any Python program state, by seeing if the constraint's formula is the "true" or
//! "false" leaf node.
//!
@ -204,18 +224,18 @@ use crate::types::{Truthiness, Type, infer_expression_type};
/// for a particular [`Predicate`], if your formula needs to consider how a particular runtime
/// property might be different at different points in the execution of the program.
///
/// Visibility constraints are normalized, so equivalent constraints are guaranteed to have equal
/// reachability constraints are normalized, so equivalent constraints are guaranteed to have equal
/// IDs.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub(crate) struct ScopedVisibilityConstraintId(u32);
pub(crate) struct ScopedReachabilityConstraintId(u32);
impl std::fmt::Debug for ScopedVisibilityConstraintId {
impl std::fmt::Debug for ScopedReachabilityConstraintId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_tuple("ScopedVisibilityConstraintId");
let mut f = f.debug_tuple("ScopedReachabilityConstraintId");
match *self {
// We use format_args instead of rendering the strings directly so that we don't get
// any quotes in the output: ScopedVisibilityConstraintId(AlwaysTrue) instead of
// ScopedVisibilityConstraintId("AlwaysTrue").
// any quotes in the output: ScopedReachabilityConstraintId(AlwaysTrue) instead of
// ScopedReachabilityConstraintId("AlwaysTrue").
ALWAYS_TRUE => f.field(&format_args!("AlwaysTrue")),
AMBIGUOUS => f.field(&format_args!("Ambiguous")),
ALWAYS_FALSE => f.field(&format_args!("AlwaysFalse")),
@ -237,34 +257,34 @@ impl std::fmt::Debug for ScopedVisibilityConstraintId {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct InteriorNode {
/// A "variable" that is evaluated as part of a TDD ternary function. For visibility
/// A "variable" that is evaluated as part of a TDD ternary function. For reachability
/// constraints, this is a `Predicate` that represents some runtime property of the Python
/// code that we are evaluating.
atom: ScopedPredicateId,
if_true: ScopedVisibilityConstraintId,
if_ambiguous: ScopedVisibilityConstraintId,
if_false: ScopedVisibilityConstraintId,
if_true: ScopedReachabilityConstraintId,
if_ambiguous: ScopedReachabilityConstraintId,
if_false: ScopedReachabilityConstraintId,
}
impl ScopedVisibilityConstraintId {
impl ScopedReachabilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId(0xffff_ffff);
pub(crate) const ALWAYS_TRUE: ScopedReachabilityConstraintId =
ScopedReachabilityConstraintId(0xffff_ffff);
/// A special ID that is used for an ambiguous constraint.
pub(crate) const AMBIGUOUS: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId(0xffff_fffe);
pub(crate) const AMBIGUOUS: ScopedReachabilityConstraintId =
ScopedReachabilityConstraintId(0xffff_fffe);
/// A special ID that is used for an "always false" / "never visible" constraint.
pub(crate) const ALWAYS_FALSE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId(0xffff_fffd);
pub(crate) const ALWAYS_FALSE: ScopedReachabilityConstraintId =
ScopedReachabilityConstraintId(0xffff_fffd);
fn is_terminal(self) -> bool {
self.0 >= SMALLEST_TERMINAL.0
}
}
impl Idx for ScopedVisibilityConstraintId {
impl Idx for ScopedReachabilityConstraintId {
#[inline]
fn new(value: usize) -> Self {
assert!(value <= (SMALLEST_TERMINAL.0 as usize));
@ -280,35 +300,41 @@ impl Idx for ScopedVisibilityConstraintId {
}
// Rebind some constants locally so that we don't need as many qualifiers below.
const ALWAYS_TRUE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_TRUE;
const AMBIGUOUS: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::AMBIGUOUS;
const ALWAYS_FALSE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_FALSE;
const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
const ALWAYS_TRUE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::ALWAYS_TRUE;
const AMBIGUOUS: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::AMBIGUOUS;
const ALWAYS_FALSE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::ALWAYS_FALSE;
const SMALLEST_TERMINAL: ScopedReachabilityConstraintId = ALWAYS_FALSE;
/// A collection of visibility constraints for a given scope.
/// A collection of reachability constraints for a given scope.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct VisibilityConstraints {
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
pub(crate) struct ReachabilityConstraints {
interiors: IndexVec<ScopedReachabilityConstraintId, InteriorNode>,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct VisibilityConstraintsBuilder {
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
interior_cache: FxHashMap<InteriorNode, ScopedVisibilityConstraintId>,
not_cache: FxHashMap<ScopedVisibilityConstraintId, ScopedVisibilityConstraintId>,
pub(crate) struct ReachabilityConstraintsBuilder {
interiors: IndexVec<ScopedReachabilityConstraintId, InteriorNode>,
interior_cache: FxHashMap<InteriorNode, ScopedReachabilityConstraintId>,
not_cache: FxHashMap<ScopedReachabilityConstraintId, ScopedReachabilityConstraintId>,
and_cache: FxHashMap<
(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
ScopedVisibilityConstraintId,
(
ScopedReachabilityConstraintId,
ScopedReachabilityConstraintId,
),
ScopedReachabilityConstraintId,
>,
or_cache: FxHashMap<
(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
ScopedVisibilityConstraintId,
(
ScopedReachabilityConstraintId,
ScopedReachabilityConstraintId,
),
ScopedReachabilityConstraintId,
>,
}
impl VisibilityConstraintsBuilder {
pub(crate) fn build(self) -> VisibilityConstraints {
VisibilityConstraints {
impl ReachabilityConstraintsBuilder {
pub(crate) fn build(self) -> ReachabilityConstraints {
ReachabilityConstraints {
interiors: self.interiors,
}
}
@ -318,8 +344,8 @@ impl VisibilityConstraintsBuilder {
/// any internal node, since they are leaf nodes.
fn cmp_atoms(
&self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
a: ScopedReachabilityConstraintId,
b: ScopedReachabilityConstraintId,
) -> Ordering {
if a == b || (a.is_terminal() && b.is_terminal()) {
Ordering::Equal
@ -332,9 +358,9 @@ impl VisibilityConstraintsBuilder {
}
}
/// Adds an interior node, ensuring that we always use the same visibility constraint ID for
/// Adds an interior node, ensuring that we always use the same reachability constraint ID for
/// equal nodes.
fn add_interior(&mut self, node: InteriorNode) -> ScopedVisibilityConstraintId {
fn add_interior(&mut self, node: InteriorNode) -> ScopedReachabilityConstraintId {
// If the true and false branches lead to the same node, we can override the ambiguous
// branch to go there too. And this node is then redundant and can be reduced.
if node.if_true == node.if_false {
@ -347,7 +373,7 @@ impl VisibilityConstraintsBuilder {
.or_insert_with(|| self.interiors.push(node))
}
/// Adds a new visibility constraint that checks a single [`Predicate`].
/// Adds a new reachability constraint that checks a single [`Predicate`].
///
/// [`ScopedPredicateId`]s are the “variables” that are evaluated by a TDD. A TDD variable has
/// the same value no matter how many times it appears in the ternary formula that the TDD
@ -361,7 +387,7 @@ impl VisibilityConstraintsBuilder {
pub(crate) fn add_atom(
&mut self,
predicate: ScopedPredicateId,
) -> ScopedVisibilityConstraintId {
) -> ScopedReachabilityConstraintId {
self.add_interior(InteriorNode {
atom: predicate,
if_true: ALWAYS_TRUE,
@ -370,11 +396,11 @@ impl VisibilityConstraintsBuilder {
})
}
/// Adds a new visibility constraint that is the ternary NOT of an existing one.
/// Adds a new reachability constraint that is the ternary NOT of an existing one.
pub(crate) fn add_not_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
a: ScopedReachabilityConstraintId,
) -> ScopedReachabilityConstraintId {
if a == ALWAYS_TRUE {
return ALWAYS_FALSE;
} else if a == AMBIGUOUS {
@ -400,12 +426,12 @@ impl VisibilityConstraintsBuilder {
result
}
/// Adds a new visibility constraint that is the ternary OR of two existing ones.
/// Adds a new reachability constraint that is the ternary OR of two existing ones.
pub(crate) fn add_or_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
a: ScopedReachabilityConstraintId,
b: ScopedReachabilityConstraintId,
) -> ScopedReachabilityConstraintId {
match (a, b) {
(ALWAYS_TRUE, _) | (_, ALWAYS_TRUE) => return ALWAYS_TRUE,
(ALWAYS_FALSE, other) | (other, ALWAYS_FALSE) => return other,
@ -466,12 +492,12 @@ impl VisibilityConstraintsBuilder {
result
}
/// Adds a new visibility constraint that is the ternary AND of two existing ones.
/// Adds a new reachability constraint that is the ternary AND of two existing ones.
pub(crate) fn add_and_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
a: ScopedReachabilityConstraintId,
b: ScopedReachabilityConstraintId,
) -> ScopedReachabilityConstraintId {
match (a, b) {
(ALWAYS_FALSE, _) | (_, ALWAYS_FALSE) => return ALWAYS_FALSE,
(ALWAYS_TRUE, other) | (other, ALWAYS_TRUE) => return other,
@ -533,13 +559,13 @@ impl VisibilityConstraintsBuilder {
}
}
impl VisibilityConstraints {
/// Analyze the statically known visibility for a given visibility constraint.
impl ReachabilityConstraints {
/// Analyze the statically known reachability for a given constraint.
pub(crate) fn evaluate<'db>(
&self,
db: &'db dyn Db,
predicates: &Predicates<'db>,
mut id: ScopedVisibilityConstraintId,
mut id: ScopedReachabilityConstraintId,
) -> Truthiness {
loop {
let node = match id {

View file

@ -221,43 +221,8 @@
//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now
//! have two live bindings of `x`: `x = 3` and `x = 4`.
//!
//! Another piece of information that the `UseDefMap` needs to provide are visibility constraints.
//! These are similar to the narrowing constraints, but apply to bindings and declarations within a
//! control flow path. Consider the following example:
//! ```py
//! x = 1
//! if test:
//! x = 2
//! y = "y"
//! ```
//! In principle, there are two possible control flow paths here. However, if we can statically
//! infer `test` to be always truthy or always falsy (that is, `__bool__` of `test` is of type
//! `Literal[True]` or `Literal[False]`), we can rule out one of the possible paths. To support
//! this feature, we record a visibility constraint of `test` to all live bindings and declarations
//! *after* visiting the body of the `if` statement. And we record a negative visibility constraint
//! `~test` to all live bindings/declarations in the (implicit) `else` branch. For the example
//! above, we would record the following visibility constraints (adding the implicit "unbound"
//! definitions for clarity):
//! ```py
//! x = <unbound> # not live, shadowed by `x = 1`
//! y = <unbound> # visibility constraint: ~test
//!
//! x = 1 # visibility constraint: ~test
//! if test:
//! x = 2 # visibility constraint: test
//! y = "y" # visibility constraint: test
//! ```
//! When we encounter a use of `x` after this `if` statement, we would record two live bindings: `x
//! = 1` with a constraint of `~test`, and `x = 2` with a constraint of `test`. In type inference,
//! when we iterate over all live bindings, we can evaluate these constraints to determine if a
//! particular binding is actually visible. For example, if `test` is always truthy, we only see
//! the `x = 2` binding. If `test` is always falsy, we only see the `x = 1` binding. And if the
//! `__bool__` method of `test` returns type `bool`, we can see both bindings.
//!
//! Note that we also record visibility constraints for the start of the scope. This is important
//! to determine if a place is definitely bound, possibly unbound, or definitely unbound. In the
//! example above, The `y = <unbound>` binding is constrained by `~test`, so `y` would only be
//! definitely-bound if `test` is always truthy.
//! Another piece of information that the `UseDefMap` needs to provide are reachability constraints.
//! See [`reachability_constraints.rs`] for more details, in particular how they apply to bindings.
//!
//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
@ -282,8 +247,8 @@ use crate::semantic_index::place::{FileScopeId, PlaceExpr, ScopeKind, ScopedPlac
use crate::semantic_index::predicate::{
Predicate, Predicates, PredicatesBuilder, ScopedPredicateId, StarImportPlaceholderPredicate,
};
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
use crate::semantic_index::reachability_constraints::{
ReachabilityConstraints, ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId,
};
use crate::types::{IntersectionBuilder, Truthiness, Type, infer_narrowing_constraint};
@ -302,14 +267,14 @@ pub(crate) struct UseDefMap<'db> {
/// Array of narrowing constraints in this scope.
narrowing_constraints: NarrowingConstraints,
/// Array of visibility constraints in this scope.
visibility_constraints: VisibilityConstraints,
/// Array of reachability constraints in this scope.
reachability_constraints: ReachabilityConstraints,
/// [`Bindings`] reaching a [`ScopedUseId`].
bindings_by_use: IndexVec<ScopedUseId, Bindings>,
/// Tracks whether or not a given AST node is reachable from the start of the scope.
node_reachability: FxHashMap<NodeKey, ScopedVisibilityConstraintId>,
node_reachability: FxHashMap<NodeKey, ScopedReachabilityConstraintId>,
/// If the definition is a binding (only) -- `x = 1` for example -- then we need
/// [`Declarations`] to know whether this binding is permitted by the live declarations.
@ -335,20 +300,24 @@ pub(crate) struct UseDefMap<'db> {
/// eager scope.
eager_snapshots: EagerSnapshots,
/// Whether or not the start of the scope is visible.
/// Whether or not the end of the scope is reachable.
///
/// This is used to check if the function can implicitly return `None`.
/// For example:
///
/// ```python
/// def f(cond: bool) -> int:
/// ```py
/// def f(cond: bool) -> int | None:
/// if cond:
/// return 1
///
/// def g() -> int:
/// if True:
/// return 1
/// ```
///
/// In this case, the function may implicitly return `None`.
/// Function `f` may implicitly return `None`, but `g` cannot.
///
/// This is used by `UseDefMap::can_implicit_return`.
scope_start_visibility: ScopedVisibilityConstraintId,
/// This is used by [`UseDefMap::can_implicitly_return_none`].
end_of_scope_reachability: ScopedReachabilityConstraintId,
}
impl<'db> UseDefMap<'db> {
@ -378,12 +347,11 @@ impl<'db> UseDefMap<'db> {
pub(super) fn is_reachable(
&self,
db: &dyn crate::Db,
reachability: ScopedVisibilityConstraintId,
reachability: ScopedReachabilityConstraintId,
) -> bool {
!self
.visibility_constraints
self.reachability_constraints
.evaluate(db, &self.predicates, reachability)
.is_always_false()
.may_be_true()
}
/// Check whether or not a given expression is reachable from the start of the scope. This
@ -392,8 +360,8 @@ impl<'db> UseDefMap<'db> {
/// analysis.
#[track_caller]
pub(super) fn is_node_reachable(&self, db: &dyn crate::Db, node_key: NodeKey) -> bool {
!self
.visibility_constraints
self
.reachability_constraints
.evaluate(
db,
&self.predicates,
@ -402,7 +370,7 @@ impl<'db> UseDefMap<'db> {
.get(&node_key)
.expect("`is_node_reachable` should only be called on AST nodes with recorded reachability"),
)
.is_always_false()
.may_be_true()
}
pub(crate) fn public_bindings(
@ -467,20 +435,23 @@ impl<'db> UseDefMap<'db> {
}
/// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`.
pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool {
pub(crate) fn can_implicitly_return_none(&self, db: &dyn crate::Db) -> bool {
!self
.visibility_constraints
.evaluate(db, &self.predicates, self.scope_start_visibility)
.reachability_constraints
.evaluate(db, &self.predicates, self.end_of_scope_reachability)
.is_always_false()
}
pub(crate) fn is_binding_visible(
pub(crate) fn is_binding_reachable(
&self,
db: &dyn crate::Db,
binding: &BindingWithConstraints<'_, 'db>,
) -> Truthiness {
self.visibility_constraints
.evaluate(db, &self.predicates, binding.visibility_constraint)
self.reachability_constraints.evaluate(
db,
&self.predicates,
binding.reachability_constraint,
)
}
fn bindings_iterator<'map>(
@ -491,7 +462,7 @@ impl<'db> UseDefMap<'db> {
all_definitions: &self.all_definitions,
predicates: &self.predicates,
narrowing_constraints: &self.narrowing_constraints,
visibility_constraints: &self.visibility_constraints,
reachability_constraints: &self.reachability_constraints,
inner: bindings.iter(),
}
}
@ -503,7 +474,7 @@ impl<'db> UseDefMap<'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
predicates: &self.predicates,
visibility_constraints: &self.visibility_constraints,
reachability_constraints: &self.reachability_constraints,
inner: declarations.iter(),
}
}
@ -538,7 +509,7 @@ pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, DefinitionState<'db>>,
pub(crate) predicates: &'map Predicates<'db>,
pub(crate) narrowing_constraints: &'map NarrowingConstraints,
pub(crate) visibility_constraints: &'map VisibilityConstraints,
pub(crate) reachability_constraints: &'map ReachabilityConstraints,
inner: LiveBindingsIterator<'map>,
}
@ -558,7 +529,7 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
constraint_ids: narrowing_constraints
.iter_predicates(live_binding.narrowing_constraint),
},
visibility_constraint: live_binding.visibility_constraint,
reachability_constraint: live_binding.reachability_constraint,
})
}
}
@ -568,7 +539,7 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: DefinitionState<'db>,
pub(crate) narrowing_constraint: ConstraintsIterator<'map, 'db>,
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
pub(crate) reachability_constraint: ScopedReachabilityConstraintId,
}
pub(crate) struct ConstraintsIterator<'map, 'db> {
@ -618,13 +589,13 @@ impl<'db> ConstraintsIterator<'_, 'db> {
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, DefinitionState<'db>>,
pub(crate) predicates: &'map Predicates<'db>,
pub(crate) visibility_constraints: &'map VisibilityConstraints,
pub(crate) reachability_constraints: &'map ReachabilityConstraints,
inner: LiveDeclarationsIterator<'map>,
}
pub(crate) struct DeclarationWithConstraint<'db> {
pub(crate) declaration: DefinitionState<'db>,
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
pub(crate) reachability_constraint: ScopedReachabilityConstraintId,
}
impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
@ -634,11 +605,11 @@ impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
self.inner.next().map(
|LiveDeclaration {
declaration,
visibility_constraint,
reachability_constraint,
}| {
DeclarationWithConstraint {
declaration: self.all_definitions[*declaration],
visibility_constraint: *visibility_constraint,
reachability_constraint: *reachability_constraint,
}
},
)
@ -651,8 +622,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
#[derive(Clone, Debug)]
pub(super) struct FlowSnapshot {
place_states: IndexVec<ScopedPlaceId, PlaceState>,
scope_start_visibility: ScopedVisibilityConstraintId,
reachability: ScopedVisibilityConstraintId,
reachability: ScopedReachabilityConstraintId,
}
#[derive(Debug)]
@ -666,60 +636,18 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Builder of narrowing constraints.
pub(super) narrowing_constraints: NarrowingConstraintsBuilder,
/// Builder of visibility constraints.
pub(super) visibility_constraints: VisibilityConstraintsBuilder,
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
/// whether or not a use of a place at the current point in control flow would see
/// the fake `x = <unbound>` binding at the start of the scope. This is important for
/// cases like the following, where we need to hide the implicit unbound binding in
/// the "else" branch:
/// ```py
/// # x = <unbound>
///
/// if True:
/// x = 1
///
/// use(x) # the `x = <unbound>` binding is not visible here
/// ```
pub(super) scope_start_visibility: ScopedVisibilityConstraintId,
/// Builder of reachability constraints.
pub(super) reachability_constraints: ReachabilityConstraintsBuilder,
/// Live bindings at each so-far-recorded use.
bindings_by_use: IndexVec<ScopedUseId, Bindings>,
/// Tracks whether or not the scope start is visible at the current point in control flow.
/// This is subtly different from `scope_start_visibility`, as we apply these constraints
/// at the beginning of a branch. Visibility constraints, on the other hand, need to be
/// applied at the end of a branch, as we apply them retroactively to all live bindings:
/// ```py
/// y = 1
///
/// if test:
/// # we record a reachability constraint of [test] here,
/// # so that it can affect the use of `x`:
///
/// x # we store a reachability constraint of [test] for this use of `x`
///
/// y = 2
///
/// # we record a visibility constraint of [test] here, which retroactively affects
/// # the `y = 1` and the `y = 2` binding.
/// else:
/// # we record a reachability constraint of [~test] here.
///
/// pass
///
/// # we record a visibility constraint of [~test] here, which retroactively affects
/// # the `y = 1` binding.
///
/// use(y)
/// ```
/// Depending on the value of `test`, the `y = 1`, `y = 2`, or both bindings may be visible.
/// The use of `x` is recorded with a reachability constraint of `[test]`.
pub(super) reachability: ScopedVisibilityConstraintId,
/// Tracks whether or not the current point in control flow is reachable from the
/// start of the scope.
pub(super) reachability: ScopedReachabilityConstraintId,
/// Tracks whether or not a given AST node is reachable from the start of the scope.
node_reachability: FxHashMap<NodeKey, ScopedVisibilityConstraintId>,
node_reachability: FxHashMap<NodeKey, ScopedReachabilityConstraintId>,
/// Live declarations for each so-far-recorded binding.
declarations_by_binding: FxHashMap<Definition<'db>, Declarations>,
@ -744,10 +672,9 @@ impl<'db> UseDefMapBuilder<'db> {
all_definitions: IndexVec::from_iter([DefinitionState::Undefined]),
predicates: PredicatesBuilder::default(),
narrowing_constraints: NarrowingConstraintsBuilder::default(),
visibility_constraints: VisibilityConstraintsBuilder::default(),
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
reachability_constraints: ReachabilityConstraintsBuilder::default(),
bindings_by_use: IndexVec::new(),
reachability: ScopedVisibilityConstraintId::ALWAYS_TRUE,
reachability: ScopedReachabilityConstraintId::ALWAYS_TRUE,
node_reachability: FxHashMap::default(),
declarations_by_binding: FxHashMap::default(),
bindings_by_declaration: FxHashMap::default(),
@ -757,14 +684,20 @@ impl<'db> UseDefMapBuilder<'db> {
}
}
pub(super) fn mark_unreachable(&mut self) {
self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE);
self.reachability = ScopedVisibilityConstraintId::ALWAYS_FALSE;
self.reachability = ScopedReachabilityConstraintId::ALWAYS_FALSE;
for state in &mut self.place_states {
state.record_reachability_constraint(
&mut self.reachability_constraints,
ScopedReachabilityConstraintId::ALWAYS_FALSE,
);
}
}
pub(super) fn add_place(&mut self, place: ScopedPlaceId) {
let new_place = self
.place_states
.push(PlaceState::undefined(self.scope_start_visibility));
.push(PlaceState::undefined(self.reachability));
debug_assert_eq!(place, new_place);
}
@ -780,7 +713,7 @@ impl<'db> UseDefMapBuilder<'db> {
.insert(binding, place_state.declarations().clone());
place_state.record_binding(
def_id,
self.scope_start_visibility,
self.reachability,
self.is_class_scope,
is_place_name,
);
@ -798,136 +731,85 @@ impl<'db> UseDefMapBuilder<'db> {
}
}
pub(super) fn record_visibility_constraint(
&mut self,
constraint: ScopedVisibilityConstraintId,
) {
for state in &mut self.place_states {
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
}
self.scope_start_visibility = self
.visibility_constraints
.add_and_constraint(self.scope_start_visibility, constraint);
}
/// Snapshot the state of a single place at the current point in control flow.
///
/// This is only used for `*`-import visibility constraints, which are handled differently
/// to most other visibility constraints. See the doc-comment for
/// [`Self::record_and_negate_star_import_visibility_constraint`] for more details.
/// This is only used for `*`-import reachability constraints, which are handled differently
/// to most other reachability constraints. See the doc-comment for
/// [`Self::record_and_negate_star_import_reachability_constraint`] for more details.
pub(super) fn single_place_snapshot(&self, place: ScopedPlaceId) -> PlaceState {
self.place_states[place].clone()
}
/// This method exists solely for handling `*`-import visibility constraints.
/// This method exists solely for handling `*`-import reachability constraints.
///
/// The reason why we add visibility constraints for [`Definition`]s created by `*` imports
/// The reason why we add reachability constraints for [`Definition`]s created by `*` imports
/// is laid out in the doc-comment for [`StarImportPlaceholderPredicate`]. But treating these
/// visibility constraints in the use-def map the same way as all other visibility constraints
/// reachability constraints in the use-def map the same way as all other reachability constraints
/// was shown to lead to [significant regressions] for small codebases where typeshed
/// dominates. (Although `*` imports are not common generally, they are used in several
/// important places by typeshed.)
///
/// To solve these regressions, it was observed that we could do significantly less work for
/// `*`-import definitions. We do a number of things differently here to our normal handling of
/// visibility constraints:
/// reachability constraints:
///
/// - We only apply and negate the visibility constraints to a single symbol, rather than to
/// - We only apply and negate the reachability constraints to a single symbol, rather than to
/// all symbols. This is possible here because, unlike most definitions, we know in advance that
/// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly
/// which definition it is.
///
/// Doing things this way is cheaper in and of itself. However, it also allows us to avoid
/// calling [`Self::simplify_visibility_constraints`] after the constraint has been applied to
/// the "if-predicate-true" branch and negated for the "if-predicate-false" branch. Simplifying
/// the visibility constraints is only important for places that did not have any new
/// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch.
///
/// - We only snapshot the state for a single place prior to the definition, rather than doing
/// expensive calls to [`Self::snapshot`]. Again, this is possible because we know
/// that only a single definition occurs inside the "if-predicate-true" predicate branch.
///
/// - Normally we take care to check whether an "if-predicate-true" branch or an
/// "if-predicate-false" branch contains a terminal statement: these can affect the visibility
/// "if-predicate-false" branch contains a terminal statement: these can affect the reachability
/// of symbols defined inside either branch. However, in the case of `*`-import definitions,
/// this is unnecessary (and therefore not done in this method), since we know that a `*`-import
/// predicate cannot create a terminal statement inside either branch.
///
/// [significant regressions]: https://github.com/astral-sh/ruff/pull/17286#issuecomment-2786755746
pub(super) fn record_and_negate_star_import_visibility_constraint(
pub(super) fn record_and_negate_star_import_reachability_constraint(
&mut self,
star_import: StarImportPlaceholderPredicate<'db>,
symbol: ScopedPlaceId,
pre_definition_state: PlaceState,
) {
let predicate_id = self.add_predicate(star_import.into());
let visibility_id = self.visibility_constraints.add_atom(predicate_id);
let negated_visibility_id = self
.visibility_constraints
.add_not_constraint(visibility_id);
let reachability_id = self.reachability_constraints.add_atom(predicate_id);
let negated_reachability_id = self
.reachability_constraints
.add_not_constraint(reachability_id);
let mut post_definition_state =
std::mem::replace(&mut self.place_states[symbol], pre_definition_state);
post_definition_state
.record_visibility_constraint(&mut self.visibility_constraints, visibility_id);
.record_reachability_constraint(&mut self.reachability_constraints, reachability_id);
self.place_states[symbol]
.record_visibility_constraint(&mut self.visibility_constraints, negated_visibility_id);
self.place_states[symbol].record_reachability_constraint(
&mut self.reachability_constraints,
negated_reachability_id,
);
self.place_states[symbol].merge(
post_definition_state,
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
&mut self.reachability_constraints,
);
}
/// This method resets the visibility constraints for all places to a previous state
/// *if* there have been no new declarations or bindings since then. Consider the
/// following example:
/// ```py
/// x = 0
/// y = 0
/// if test_a:
/// y = 1
/// elif test_b:
/// y = 2
/// elif test_c:
/// y = 3
///
/// # RESET
/// ```
/// We build a complex visibility constraint for the `y = 0` binding. We build the same
/// constraint for the `x = 0` binding as well, but at the `RESET` point, we can get rid
/// of it, as the `if`-`elif`-`elif` chain doesn't include any new bindings of `x`.
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
debug_assert!(self.place_states.len() >= snapshot.place_states.len());
// If there are any control flow paths that have become unreachable between `snapshot` and
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
if self.scope_start_visibility != snapshot.scope_start_visibility {
return;
}
// Note that this loop terminates when we reach a place not present in the snapshot.
// This means we keep visibility constraints for all new places, which is intended,
// since these places have been introduced in the corresponding branch, which might
// be subject to visibility constraints. We only simplify/reset visibility constraints
// for places that have the same bindings and declarations present compared to the
// snapshot.
for (current, snapshot) in self.place_states.iter_mut().zip(snapshot.place_states) {
current.simplify_visibility_constraints(snapshot);
}
}
pub(super) fn record_reachability_constraint(
&mut self,
constraint: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
constraint: ScopedReachabilityConstraintId,
) {
self.reachability = self
.visibility_constraints
.reachability_constraints
.add_and_constraint(self.reachability, constraint);
self.reachability
for state in &mut self.place_states {
state.record_reachability_constraint(&mut self.reachability_constraints, constraint);
}
}
pub(super) fn record_declaration(
@ -941,7 +823,7 @@ impl<'db> UseDefMapBuilder<'db> {
let place_state = &mut self.place_states[place];
self.bindings_by_declaration
.insert(declaration, place_state.bindings().clone());
place_state.record_declaration(def_id);
place_state.record_declaration(def_id, self.reachability);
}
pub(super) fn record_declaration_and_binding(
@ -956,10 +838,10 @@ impl<'db> UseDefMapBuilder<'db> {
.all_definitions
.push(DefinitionState::Defined(definition));
let place_state = &mut self.place_states[place];
place_state.record_declaration(def_id);
place_state.record_declaration(def_id, self.reachability);
place_state.record_binding(
def_id,
self.scope_start_visibility,
self.reachability,
self.is_class_scope,
is_place_name,
);
@ -970,7 +852,7 @@ impl<'db> UseDefMapBuilder<'db> {
let place_state = &mut self.place_states[place];
place_state.record_binding(
def_id,
self.scope_start_visibility,
self.reachability,
self.is_class_scope,
is_place_name,
);
@ -1024,7 +906,6 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn snapshot(&self) -> FlowSnapshot {
FlowSnapshot {
place_states: self.place_states.clone(),
scope_start_visibility: self.scope_start_visibility,
reachability: self.reachability,
}
}
@ -1039,16 +920,13 @@ impl<'db> UseDefMapBuilder<'db> {
// Restore the current visible-definitions state to the given snapshot.
self.place_states = snapshot.place_states;
self.scope_start_visibility = snapshot.scope_start_visibility;
self.reachability = snapshot.reachability;
// If the snapshot we are restoring is missing some places we've recorded since, we need
// to fill them in so the place IDs continue to line up. Since they don't exist in the
// snapshot, the correct state to fill them in with is "undefined".
self.place_states.resize(
num_places,
PlaceState::undefined(self.scope_start_visibility),
);
self.place_states
.resize(num_places, PlaceState::undefined(self.reachability));
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
@ -1059,13 +937,13 @@ impl<'db> UseDefMapBuilder<'db> {
// unreachable, we can leave it out of the merged result entirely. Note that we cannot
// perform any type inference at this point, so this is largely limited to unreachability
// via terminal statements. If a flow's reachability depends on an expression in the code,
// we will include the flow in the merged result; the visibility constraints of its
// we will include the flow in the merged result; the reachability constraints of its
// bindings will include this reachability condition, so that later during type inference,
// we can determine whether any particular binding is non-visible due to unreachability.
if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
if snapshot.reachability == ScopedReachabilityConstraintId::ALWAYS_FALSE {
return;
}
if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
if self.reachability == ScopedReachabilityConstraintId::ALWAYS_FALSE {
self.restore(snapshot);
return;
}
@ -1081,24 +959,20 @@ impl<'db> UseDefMapBuilder<'db> {
current.merge(
snapshot,
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
&mut self.reachability_constraints,
);
} else {
current.merge(
PlaceState::undefined(snapshot.scope_start_visibility),
PlaceState::undefined(snapshot.reachability),
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
&mut self.reachability_constraints,
);
// Place not present in snapshot, so it's unbound/undeclared from that path.
}
}
self.scope_start_visibility = self
.visibility_constraints
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
self.reachability = self
.visibility_constraints
.reachability_constraints
.add_or_constraint(self.reachability, snapshot.reachability);
}
@ -1115,14 +989,14 @@ impl<'db> UseDefMapBuilder<'db> {
all_definitions: self.all_definitions,
predicates: self.predicates.build(),
narrowing_constraints: self.narrowing_constraints.build(),
visibility_constraints: self.visibility_constraints.build(),
reachability_constraints: self.reachability_constraints.build(),
bindings_by_use: self.bindings_by_use,
node_reachability: self.node_reachability,
public_places: self.place_states,
declarations_by_binding: self.declarations_by_binding,
bindings_by_declaration: self.bindings_by_declaration,
eager_snapshots: self.eager_snapshots,
scope_start_visibility: self.scope_start_visibility,
end_of_scope_reachability: self.reachability,
}
}
}

View file

@ -49,8 +49,8 @@ use smallvec::{SmallVec, smallvec};
use crate::semantic_index::narrowing_constraints::{
NarrowingConstraintsBuilder, ScopedNarrowingConstraint, ScopedNarrowingConstraintPredicate,
};
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
use crate::semantic_index::reachability_constraints::{
ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId,
};
/// A newtype-index for a definition in a particular scope.
@ -76,7 +76,7 @@ impl ScopedDefinitionId {
const INLINE_DEFINITIONS_PER_PLACE: usize = 4;
/// Live declarations for a single place at some point in control flow, with their
/// corresponding visibility constraints.
/// corresponding reachability constraints.
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
pub(super) struct Declarations {
/// A list of live declarations for this place, sorted by their `ScopedDefinitionId`
@ -87,16 +87,16 @@ pub(super) struct Declarations {
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct LiveDeclaration {
pub(super) declaration: ScopedDefinitionId,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
pub(super) reachability_constraint: ScopedReachabilityConstraintId,
}
pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>;
impl Declarations {
fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
fn undeclared(reachability_constraint: ScopedReachabilityConstraintId) -> Self {
let initial_declaration = LiveDeclaration {
declaration: ScopedDefinitionId::UNBOUND,
visibility_constraint: scope_start_visibility,
reachability_constraint,
};
Self {
live_declarations: smallvec![initial_declaration],
@ -104,24 +104,28 @@ impl Declarations {
}
/// Record a newly-encountered declaration for this place.
fn record_declaration(&mut self, declaration: ScopedDefinitionId) {
fn record_declaration(
&mut self,
declaration: ScopedDefinitionId,
reachability_constraint: ScopedReachabilityConstraintId,
) {
// The new declaration replaces all previous live declaration in this path.
self.live_declarations.clear();
self.live_declarations.push(LiveDeclaration {
declaration,
visibility_constraint: ScopedVisibilityConstraintId::ALWAYS_TRUE,
reachability_constraint,
});
}
/// Add given visibility constraint to all live declarations.
pub(super) fn record_visibility_constraint(
/// Add given reachability constraint to all live declarations.
pub(super) fn record_reachability_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
reachability_constraints: &mut ReachabilityConstraintsBuilder,
constraint: ScopedReachabilityConstraintId,
) {
for declaration in &mut self.live_declarations {
declaration.visibility_constraint = visibility_constraints
.add_and_constraint(declaration.visibility_constraint, constraint);
declaration.reachability_constraint = reachability_constraints
.add_and_constraint(declaration.reachability_constraint, constraint);
}
}
@ -130,46 +134,24 @@ impl Declarations {
self.live_declarations.iter()
}
/// Iterate over the IDs of each currently live declaration for this place
fn iter_declarations(&self) -> impl Iterator<Item = ScopedDefinitionId> + '_ {
self.iter().map(|lb| lb.declaration)
}
fn simplify_visibility_constraints(&mut self, other: Declarations) {
// If the set of live declarations hasn't changed, don't simplify.
if self.live_declarations.len() != other.live_declarations.len()
|| !self.iter_declarations().eq(other.iter_declarations())
{
return;
}
for (declaration, other_declaration) in self
.live_declarations
.iter_mut()
.zip(other.live_declarations)
{
declaration.visibility_constraint = other_declaration.visibility_constraint;
}
}
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
fn merge(&mut self, b: Self, reachability_constraints: &mut ReachabilityConstraintsBuilder) {
let a = std::mem::take(self);
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
// the merged `live_declarations` vec remains sorted. If a definition is found in both `a`
// and `b`, we compose the constraints from the two paths in an appropriate way
// (intersection for narrowing constraints; ternary OR for visibility constraints). If a
// (intersection for narrowing constraints; ternary OR for reachability constraints). If a
// definition is found in only one path, it is used as-is.
let a = a.live_declarations.into_iter();
let b = b.live_declarations.into_iter();
for zipped in a.merge_join_by(b, |a, b| a.declaration.cmp(&b.declaration)) {
match zipped {
EitherOrBoth::Both(a, b) => {
let visibility_constraint = visibility_constraints
.add_or_constraint(a.visibility_constraint, b.visibility_constraint);
let reachability_constraint = reachability_constraints
.add_or_constraint(a.reachability_constraint, b.reachability_constraint);
self.live_declarations.push(LiveDeclaration {
declaration: a.declaration,
visibility_constraint,
reachability_constraint,
});
}
@ -193,7 +175,7 @@ pub(super) enum EagerSnapshot {
}
/// Live bindings for a single place at some point in control flow. Each live binding comes
/// with a set of narrowing constraints and a visibility constraint.
/// with a set of narrowing constraints and a reachability constraint.
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
pub(super) struct Bindings {
/// The narrowing constraint applicable to the "unbound" binding, if we need access to it even
@ -217,17 +199,17 @@ impl Bindings {
pub(super) struct LiveBinding {
pub(super) binding: ScopedDefinitionId,
pub(super) narrowing_constraint: ScopedNarrowingConstraint,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
pub(super) reachability_constraint: ScopedReachabilityConstraintId,
}
pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>;
impl Bindings {
fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self {
let initial_binding = LiveBinding {
binding: ScopedDefinitionId::UNBOUND,
narrowing_constraint: ScopedNarrowingConstraint::empty(),
visibility_constraint: scope_start_visibility,
reachability_constraint,
};
Self {
unbound_narrowing_constraint: None,
@ -239,7 +221,7 @@ impl Bindings {
pub(super) fn record_binding(
&mut self,
binding: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
reachability_constraint: ScopedReachabilityConstraintId,
is_class_scope: bool,
is_place_name: bool,
) {
@ -254,7 +236,7 @@ impl Bindings {
self.live_bindings.push(LiveBinding {
binding,
narrowing_constraint: ScopedNarrowingConstraint::empty(),
visibility_constraint,
reachability_constraint,
});
}
@ -270,15 +252,15 @@ impl Bindings {
}
}
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
/// Add given reachability constraint to all live bindings.
pub(super) fn record_reachability_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
reachability_constraints: &mut ReachabilityConstraintsBuilder,
constraint: ScopedReachabilityConstraintId,
) {
for binding in &mut self.live_bindings {
binding.visibility_constraint = visibility_constraints
.add_and_constraint(binding.visibility_constraint, constraint);
binding.reachability_constraint = reachability_constraints
.add_and_constraint(binding.reachability_constraint, constraint);
}
}
@ -287,29 +269,11 @@ impl Bindings {
self.live_bindings.iter()
}
/// Iterate over the IDs of each currently live binding for this place
fn iter_bindings(&self) -> impl Iterator<Item = ScopedDefinitionId> + '_ {
self.iter().map(|lb| lb.binding)
}
fn simplify_visibility_constraints(&mut self, other: Bindings) {
// If the set of live bindings hasn't changed, don't simplify.
if self.live_bindings.len() != other.live_bindings.len()
|| !self.iter_bindings().eq(other.iter_bindings())
{
return;
}
for (binding, other_binding) in self.live_bindings.iter_mut().zip(other.live_bindings) {
binding.visibility_constraint = other_binding.visibility_constraint;
}
}
fn merge(
&mut self,
b: Self,
narrowing_constraints: &mut NarrowingConstraintsBuilder,
visibility_constraints: &mut VisibilityConstraintsBuilder,
reachability_constraints: &mut ReachabilityConstraintsBuilder,
) {
let a = std::mem::take(self);
@ -324,7 +288,7 @@ impl Bindings {
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
// the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and
// `b`, we compose the constraints from the two paths in an appropriate way (intersection
// for narrowing constraints; ternary OR for visibility constraints). If a definition is
// for narrowing constraints; ternary OR for reachability constraints). If a definition is
// found in only one path, it is used as-is.
let a = a.live_bindings.into_iter();
let b = b.live_bindings.into_iter();
@ -337,14 +301,14 @@ impl Bindings {
let narrowing_constraint = narrowing_constraints
.intersect_constraints(a.narrowing_constraint, b.narrowing_constraint);
// For visibility constraints, we merge them using a ternary OR operation:
let visibility_constraint = visibility_constraints
.add_or_constraint(a.visibility_constraint, b.visibility_constraint);
// For reachability constraints, we merge them using a ternary OR operation:
let reachability_constraint = reachability_constraints
.add_or_constraint(a.reachability_constraint, b.reachability_constraint);
self.live_bindings.push(LiveBinding {
binding: a.binding,
narrowing_constraint,
visibility_constraint,
reachability_constraint,
});
}
@ -364,10 +328,10 @@ pub(in crate::semantic_index) struct PlaceState {
impl PlaceState {
/// Return a new [`PlaceState`] representing an unbound, undeclared place.
pub(super) fn undefined(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
pub(super) fn undefined(reachability: ScopedReachabilityConstraintId) -> Self {
Self {
declarations: Declarations::undeclared(scope_start_visibility),
bindings: Bindings::unbound(scope_start_visibility),
declarations: Declarations::undeclared(reachability),
bindings: Bindings::unbound(reachability),
}
}
@ -375,14 +339,14 @@ impl PlaceState {
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
reachability_constraint: ScopedReachabilityConstraintId,
is_class_scope: bool,
is_place_name: bool,
) {
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
self.bindings.record_binding(
binding_id,
visibility_constraint,
reachability_constraint,
is_class_scope,
is_place_name,
);
@ -398,31 +362,26 @@ impl PlaceState {
.record_narrowing_constraint(narrowing_constraints, constraint);
}
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
/// Add given reachability constraint to all live bindings.
pub(super) fn record_reachability_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
reachability_constraints: &mut ReachabilityConstraintsBuilder,
constraint: ScopedReachabilityConstraintId,
) {
self.bindings
.record_visibility_constraint(visibility_constraints, constraint);
.record_reachability_constraint(reachability_constraints, constraint);
self.declarations
.record_visibility_constraint(visibility_constraints, constraint);
}
/// Simplifies this snapshot to have the same visibility constraints as a previous point in the
/// control flow, but only if the set of live bindings or declarations for this place hasn't
/// changed.
pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: PlaceState) {
self.bindings
.simplify_visibility_constraints(snapshot_state.bindings);
self.declarations
.simplify_visibility_constraints(snapshot_state.declarations);
.record_reachability_constraint(reachability_constraints, constraint);
}
/// Record a newly-encountered declaration of this place.
pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.declarations.record_declaration(declaration_id);
pub(super) fn record_declaration(
&mut self,
declaration_id: ScopedDefinitionId,
reachability_constraint: ScopedReachabilityConstraintId,
) {
self.declarations
.record_declaration(declaration_id, reachability_constraint);
}
/// Merge another [`PlaceState`] into this one.
@ -430,12 +389,12 @@ impl PlaceState {
&mut self,
b: PlaceState,
narrowing_constraints: &mut NarrowingConstraintsBuilder,
visibility_constraints: &mut VisibilityConstraintsBuilder,
reachability_constraints: &mut ReachabilityConstraintsBuilder,
) {
self.bindings
.merge(b.bindings, narrowing_constraints, visibility_constraints);
.merge(b.bindings, narrowing_constraints, reachability_constraints);
self.declarations
.merge(b.declarations, visibility_constraints);
.merge(b.declarations, reachability_constraints);
}
pub(super) fn bindings(&self) -> &Bindings {
@ -488,7 +447,7 @@ mod tests {
.map(
|LiveDeclaration {
declaration,
visibility_constraint: _,
reachability_constraint: _,
}| {
if *declaration == ScopedDefinitionId::UNBOUND {
"undeclared".into()
@ -504,7 +463,7 @@ mod tests {
#[test]
fn unbound() {
let narrowing_constraints = NarrowingConstraintsBuilder::default();
let sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
assert_bindings(&narrowing_constraints, &sym, &["unbound<>"]);
}
@ -512,10 +471,10 @@ mod tests {
#[test]
fn with() {
let narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
@ -526,10 +485,10 @@ mod tests {
#[test]
fn record_constraint() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
@ -542,23 +501,23 @@ mod tests {
#[test]
fn merge() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut reachability_constraints = ReachabilityConstraintsBuilder::default();
// merging the same definition with the same constraint keeps the constraint
let mut sym1a = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym1a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym1a.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
let predicate = ScopedPredicateId::from_u32(0).into();
sym1a.record_narrowing_constraint(&mut narrowing_constraints, predicate);
let mut sym1b = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym1b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
@ -568,26 +527,26 @@ mod tests {
sym1a.merge(
sym1b,
&mut narrowing_constraints,
&mut visibility_constraints,
&mut reachability_constraints,
);
let mut sym1 = sym1a;
assert_bindings(&narrowing_constraints, &sym1, &["1<0>"]);
// merging the same definition with differing constraints drops all constraints
let mut sym2a = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym2a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym2a.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
let predicate = ScopedPredicateId::from_u32(1).into();
sym2a.record_narrowing_constraint(&mut narrowing_constraints, predicate);
let mut sym1b = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym1b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
@ -597,28 +556,28 @@ mod tests {
sym2a.merge(
sym1b,
&mut narrowing_constraints,
&mut visibility_constraints,
&mut reachability_constraints,
);
let sym2 = sym2a;
assert_bindings(&narrowing_constraints, &sym2, &["2<>"]);
// merging a constrained definition with unbound keeps both
let mut sym3a = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let mut sym3a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym3a.record_binding(
ScopedDefinitionId::from_u32(3),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
ScopedReachabilityConstraintId::ALWAYS_TRUE,
false,
true,
);
let predicate = ScopedPredicateId::from_u32(3).into();
sym3a.record_narrowing_constraint(&mut narrowing_constraints, predicate);
let sym2b = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let sym2b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym3a.merge(
sym2b,
&mut narrowing_constraints,
&mut visibility_constraints,
&mut reachability_constraints,
);
let sym3 = sym3a;
assert_bindings(&narrowing_constraints, &sym3, &["unbound<>", "3<3>"]);
@ -627,7 +586,7 @@ mod tests {
sym1.merge(
sym3,
&mut narrowing_constraints,
&mut visibility_constraints,
&mut reachability_constraints,
);
let sym = sym1;
assert_bindings(&narrowing_constraints, &sym, &["unbound<>", "1<0>", "3<3>"]);
@ -635,24 +594,33 @@ mod tests {
#[test]
fn no_declaration() {
let sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
assert_declarations(&sym, &["undeclared"]);
}
#[test]
fn record_declaration() {
let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(
ScopedDefinitionId::from_u32(1),
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
assert_declarations(&sym, &["1"]);
}
#[test]
fn record_declaration_override() {
let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
sym.record_declaration(ScopedDefinitionId::from_u32(2));
let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(
ScopedDefinitionId::from_u32(1),
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
sym.record_declaration(
ScopedDefinitionId::from_u32(2),
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
assert_declarations(&sym, &["2"]);
}
@ -660,17 +628,23 @@ mod tests {
#[test]
fn record_declaration_merge() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut reachability_constraints = ReachabilityConstraintsBuilder::default();
let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(
ScopedDefinitionId::from_u32(1),
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
let mut sym2 = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
let mut sym2 = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym2.record_declaration(
ScopedDefinitionId::from_u32(2),
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
sym.merge(
sym2,
&mut narrowing_constraints,
&mut visibility_constraints,
&mut reachability_constraints,
);
assert_declarations(&sym, &["1", "2"]);
@ -679,16 +653,19 @@ mod tests {
#[test]
fn record_declaration_merge_partial_undeclared() {
let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut reachability_constraints = ReachabilityConstraintsBuilder::default();
let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(
ScopedDefinitionId::from_u32(1),
ScopedReachabilityConstraintId::ALWAYS_TRUE,
);
let sym2 = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
let sym2 = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
sym.merge(
sym2,
&mut narrowing_constraints,
&mut visibility_constraints,
&mut reachability_constraints,
);
assert_declarations(&sym, &["undeclared", "1"]);

View file

@ -7052,6 +7052,10 @@ impl Truthiness {
matches!(self, Truthiness::AlwaysFalse)
}
pub(crate) const fn may_be_true(self) -> bool {
!self.is_always_false()
}
pub(crate) const fn is_always_true(self) -> bool {
matches!(self, Truthiness::AlwaysTrue)
}

View file

@ -1637,8 +1637,8 @@ impl<'db> ClassLiteral<'db> {
let method_scope = method_scope_id.to_scope_id(db, file);
let method_map = use_def_map(db, method_scope);
// The attribute assignment inherits the visibility of the method which contains it
let is_method_visible =
// The attribute assignment inherits the reachability of the method which contains it
let is_method_reachable =
if let Some(method_def) = method_scope.node(db).as_function(&module) {
let method = index.expect_single_definition(method_def);
let method_place = class_table.place_id_by_name(&method_def.name).unwrap();
@ -1646,13 +1646,13 @@ impl<'db> ClassLiteral<'db> {
.public_bindings(method_place)
.find_map(|bind| {
(bind.binding.is_defined_and(|def| def == method))
.then(|| class_map.is_binding_visible(db, &bind))
.then(|| class_map.is_binding_reachable(db, &bind))
})
.unwrap_or(Truthiness::AlwaysFalse)
} else {
Truthiness::AlwaysFalse
};
if is_method_visible.is_always_false() {
if is_method_reachable.is_always_false() {
continue;
}
@ -1663,7 +1663,7 @@ impl<'db> ClassLiteral<'db> {
for attribute_assignment in attribute_assignments {
if let DefinitionState::Undefined = attribute_assignment.binding {
// Store the implicit unbound binding here so that we can delay the
// computation of `unbound_visibility` to the point when we actually
// computation of `unbound_reachability` to the point when we actually
// need it. This is an optimization for the common case where the
// `unbound` binding is the only binding of the `name` attribute,
// i.e. if there is no `self.name = …` assignment in this method.
@ -1675,8 +1675,8 @@ impl<'db> ClassLiteral<'db> {
continue;
};
match method_map
.is_binding_visible(db, &attribute_assignment)
.and(is_method_visible)
.is_binding_reachable(db, &attribute_assignment)
.and(is_method_reachable)
{
Truthiness::AlwaysTrue => {
is_attribute_bound = Truthiness::AlwaysTrue;
@ -1691,17 +1691,17 @@ impl<'db> ClassLiteral<'db> {
}
}
// There is at least one attribute assignment that may be visible,
// so if `unbound_visibility` is always false then this attribute is considered bound.
// TODO: this is incomplete logic since the attributes bound after termination are considered visible.
let unbound_visibility = unbound_binding
// There is at least one attribute assignment that may be reachable, so if `unbound_reachability` is
// always false then this attribute is considered bound.
// TODO: this is incomplete logic since the attributes bound after termination are considered reachable.
let unbound_reachability = unbound_binding
.as_ref()
.map(|binding| method_map.is_binding_visible(db, binding))
.map(|binding| method_map.is_binding_reachable(db, binding))
.unwrap_or(Truthiness::AlwaysFalse);
if unbound_visibility
if unbound_reachability
.negate()
.and(is_method_visible)
.and(is_method_reachable)
.is_always_true()
{
is_attribute_bound = Truthiness::AlwaysTrue;

View file

@ -1853,6 +1853,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
function.known(self.db()),
Some(KnownFunction::Overload | KnownFunction::AbstractMethod)
),
Type::Never => {
// In unreachable code, we infer `Never` for decorators like `typing.overload`.
// Return `true` here to avoid false positive `invalid-return-type` lints for
// `@overload`ed functions without a body in unreachable code.
true
}
_ => false,
}
})
@ -1967,7 +1973,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
let use_def = self.index.use_def_map(scope_id);
if use_def.can_implicit_return(self.db())
if use_def.can_implicitly_return_none(self.db())
&& !Type::none(self.db()).is_assignable_to(self.db(), expected_ty)
{
let no_return = self.return_types_and_ranges.is_empty();
@ -9200,6 +9206,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} = subscript;
match value_ty {
Type::Never => {
// This case can be entered when we use a type annotation like `Literal[1]`
// in unreachable code, since we infer `Never` for `Literal`. We call
// `infer_expression` (instead of `infer_type_expression`) here to avoid
// false-positive `invalid-type-form` diagnostics (`1` is not a valid type
// expression).
self.infer_expression(&subscript.slice);
Type::unknown()
}
Type::ClassLiteral(literal) if literal.is_known(self.db(), KnownClass::Any) => {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic("Type `typing.Any` expected no type parameter");
@ -10303,16 +10318,16 @@ mod tests {
}
/// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing
/// visibility-constraint checks in that scope.
/// reachability-constraint checks in that scope.
#[test]
fn unbound_symbol_no_visibility_constraint_check() {
fn unbound_symbol_no_reachability_constraint_check() {
let mut db = setup_db();
// If the bug we are testing for is not fixed, what happens is that when inferring the
// `flag: bool = True` definitions, we look up `bool` as a deferred name (thus from end of
// scope), and because of the early return its "unbound" binding has a visibility
// scope), and because of the early return its "unbound" binding has a reachability
// constraint of `~flag`, which we evaluate, meaning we have to evaluate the definition of
// `flag` -- and we are in a cycle. With the fix, we short-circuit evaluating visibility
// `flag` -- and we are in a cycle. With the fix, we short-circuit evaluating reachability
// constraints on "unbound" if a symbol is otherwise not bound.
db.write_dedented(
"src/a.py",