Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b165ba7bd1b2a0112ce574a082ab8ea5102252ac" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
Expand Down
23 changes: 16 additions & 7 deletions crates/red_knot_python_semantic/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,15 +663,24 @@ fn symbol_from_bindings_impl<'db>(
requires_explicit_reexport.is_yes() && !binding.is_reexported(db)
};

let unbound_visibility = match bindings_with_constraints.peek() {
let unbound_visibility_constraint = match bindings_with_constraints.peek() {
Some(BindingWithConstraints {
binding,
visibility_constraint,
narrowing_constraint: _,
}) if binding.is_none_or(is_non_exported) => {
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,
}) if binding.is_none_or(is_non_exported) => Some(*visibility_constraint),
_ => None,
};

// 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
// expressions, which is extra work and can lead to cycles.
let unbound_visibility = || {
Comment thread
carljm marked this conversation as resolved.
unbound_visibility_constraint
.map(|visibility_constraint| {
visibility_constraints.evaluate(db, predicates, visibility_constraint)
})
.unwrap_or(Truthiness::AlwaysFalse)
};

let mut types = bindings_with_constraints.filter_map(
Expand Down Expand Up @@ -733,7 +742,7 @@ fn symbol_from_bindings_impl<'db>(
// 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() {
if unbound_visibility().is_always_false() {
// The scope-start is not visible
return Some(Type::Never);
}
Expand Down Expand Up @@ -762,7 +771,7 @@ fn symbol_from_bindings_impl<'db>(
);

if let Some(first) = types.next() {
let boundness = match unbound_visibility {
let boundness = match unbound_visibility() {
Truthiness::AlwaysTrue => {
unreachable!("If we have at least one binding, the scope-start should not be definitely visible")
}
Expand Down
44 changes: 44 additions & 0 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7847,6 +7847,50 @@ mod tests {
check_typevar("Y", None, None, None);
}

/// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing
/// visibility-constraint checks in that scope.
#[test]
fn unbound_symbol_no_visibility_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
// 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
// constraints on "unbound" if a symbol is otherwise not bound.
db.write_dedented(
"src/a.py",
"
from __future__ import annotations

def f():
flag: bool = True
if flag:
return True
",
)
.unwrap();

db.clear_salsa_events();
assert_file_diagnostics(&db, "src/a.py", &[]);
let events = db.take_salsa_events();
let cycles = salsa::plumbing::attach(&db, || {
events
.iter()
.filter_map(|event| {
if let salsa::EventKind::WillIterateCycle { database_key, .. } = event.kind {
Some(format!("{database_key:?}"))
} else {
None
}
})
.collect::<Vec<_>>()
});
let expected: Vec<String> = vec![];
assert_eq!(cycles, expected);
}

// Incremental inference tests
#[track_caller]
fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {
Expand Down
2 changes: 1 addition & 1 deletion fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" }
ruff_text_size = { path = "../crates/ruff_text_size" }

libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b165ba7bd1b2a0112ce574a082ab8ea5102252ac" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" }
similar = { version = "2.5.0" }
tracing = { version = "0.1.40" }

Expand Down
Loading