Skip to content

Commit d5dafc5

Browse files
committed
Evaluate reachability of non-definitely-bound to Ambiguous
1 parent 008bbfd commit d5dafc5

File tree

4 files changed

+154
-7
lines changed

4 files changed

+154
-7
lines changed

crates/ty_python_semantic/src/place.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ impl<'db> Place<'db> {
8585
}
8686
}
8787

88+
pub(crate) fn is_definitely_bound(&self) -> bool {
89+
match self {
90+
Place::Type(_, Boundness::Bound) => true,
91+
_ => false,
92+
}
93+
}
94+
8895
#[cfg(test)]
8996
#[track_caller]
9097
pub(crate) fn expect_type(self) -> Type<'db> {

crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
196196
use std::cmp::Ordering;
197197

198+
use ruff_db::parsed::parsed_module;
198199
use ruff_index::{Idx, IndexVec};
199200
use rustc_hash::FxHashMap;
200201

@@ -208,7 +209,8 @@ use crate::semantic_index::predicate::{
208209
Predicates, ScopedPredicateId,
209210
};
210211
use crate::types::{
211-
IntersectionBuilder, Truthiness, Type, UnionBuilder, UnionType, infer_expression_type,
212+
IntersectionBuilder, Truthiness, Type, UnionBuilder, UnionType, infer_expression,
213+
infer_expression_type, infer_expression_types,
212214
};
213215

214216
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
@@ -791,7 +793,15 @@ impl ReachabilityConstraints {
791793
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
792794
match predicate.node {
793795
PredicateNode::Expression(test_expr) => {
794-
let ty = infer_expression_type(db, test_expr);
796+
// The cycles happen here so the check should happen before
797+
let inference = infer_expression(db, test_expr);
798+
if !inference.definitely_bound() {
799+
return Truthiness::Ambiguous;
800+
}
801+
// let ty = infer_expression_type(db, test_expr);
802+
let file = test_expr.file(db);
803+
let module = parsed_module(db, file).load(db);
804+
let ty = inference.expression_type(test_expr.node_ref(db, &module));
795805
ty.bool(db).negate_if(!predicate.is_positive)
796806
}
797807
PredicateNode::ReturnsNever(CallableAndCallExpr {

crates/ty_python_semantic/src/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ pub(crate) use self::cyclic::{PairVisitor, TypeTransformer};
2424
pub use self::diagnostic::TypeCheckDiagnostics;
2525
pub(crate) use self::diagnostic::register_lints;
2626
pub(crate) use self::infer::{
27-
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
28-
infer_scope_types,
27+
infer_deferred_types, infer_definition_types, infer_expression, infer_expression_type,
28+
infer_expression_types, infer_scope_types,
2929
};
3030
pub(crate) use self::signatures::{CallableSignature, Signature};
3131
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,28 @@ pub(crate) fn infer_expression_type<'db>(
311311
infer_same_file_expression_type(db, expression, &module)
312312
}
313313

314+
/// TODO: Just created to verify the idea.
315+
#[salsa::tracked(returns(ref), cycle_fn=expression_cycle_recover, cycle_initial=expression_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)]
316+
pub(crate) fn infer_expression<'db>(
317+
db: &'db dyn Db,
318+
expression: Expression<'db>,
319+
) -> ExpressionInference<'db> {
320+
let file = expression.file(db);
321+
let module = parsed_module(db, file).load(db);
322+
let _span = tracing::trace_span!(
323+
"infer_expression",
324+
expression = ?expression.as_id(),
325+
range = ?expression.node_ref(db, &module).range(),
326+
?file
327+
)
328+
.entered();
329+
330+
let index = semantic_index(db, file);
331+
332+
TypeInferenceBuilder::new(db, InferenceRegion::Expression(expression), index, &module)
333+
.finish_expression()
334+
}
335+
314336
fn single_expression_cycle_recover<'db>(
315337
_db: &'db dyn Db,
316338
_value: &Type<'db>,
@@ -640,6 +662,9 @@ struct ExpressionInferenceExtra<'db> {
640662
///
641663
/// Falls back to `Type::Never` if an expression is missing.
642664
cycle_fallback: bool,
665+
666+
/// `true` if all expressions in this expression are definitely bound
667+
all_definitely_bound: bool,
643668
}
644669

645670
impl<'db> ExpressionInference<'db> {
@@ -648,6 +673,7 @@ impl<'db> ExpressionInference<'db> {
648673
Self {
649674
extra: Some(Box::new(ExpressionInferenceExtra {
650675
cycle_fallback: true,
676+
all_definitely_bound: true,
651677
..ExpressionInferenceExtra::default()
652678
})),
653679
expressions: FxHashMap::default(),
@@ -681,6 +707,13 @@ impl<'db> ExpressionInference<'db> {
681707
fn fallback_type(&self) -> Option<Type<'db>> {
682708
self.is_cycle_callback().then_some(Type::Never)
683709
}
710+
711+
pub(crate) fn definitely_bound(&self) -> bool {
712+
match self.extra.as_ref() {
713+
Some(e) => e.all_definitely_bound,
714+
None => true,
715+
}
716+
}
684717
}
685718

686719
/// Whether the intersection type is on the left or right side of the comparison.
@@ -818,6 +851,8 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
818851
///
819852
/// This is used only when constructing a cycle-recovery `TypeInference`.
820853
cycle_fallback: bool,
854+
855+
all_definitely_bound: bool,
821856
}
822857

823858
impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
@@ -849,6 +884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
849884
declarations: VecMap::default(),
850885
deferred: VecSet::default(),
851886
cycle_fallback: false,
887+
all_definitely_bound: true,
852888
}
853889
}
854890

@@ -6260,7 +6296,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
62606296
let db = self.db();
62616297

62626298
let (resolved, constraint_keys) =
6263-
self.infer_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node));
6299+
self.record_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node));
62646300

62656301
resolved
62666302
// Not found in the module's explicitly declared global symbols?
@@ -6352,6 +6388,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
63526388
}
63536389
}
63546390

6391+
/// Infer the type of a place expression from definitions, assuming a load context.
6392+
/// This method also returns the [`ConstraintKey`]s for each scope associated with `expr`,
6393+
/// which is used to narrow by condition rather than by assignment.
6394+
fn record_place_load(
6395+
&mut self,
6396+
place_expr: PlaceExprRef,
6397+
expr_ref: ast::ExprRef,
6398+
) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) {
6399+
let place = self.infer_place_load(place_expr, expr_ref);
6400+
if !place.0.place.is_definitely_bound() {
6401+
self.all_definitely_bound = false;
6402+
}
6403+
6404+
place
6405+
}
6406+
63556407
/// Infer the type of a place expression from definitions, assuming a load context.
63566408
/// This method also returns the [`ConstraintKey`]s for each scope associated with `expr`,
63576409
/// which is used to narrow by condition rather than by assignment.
@@ -6788,7 +6840,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
67886840

67896841
let mut assigned_type = None;
67906842
if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) {
6791-
let (resolved, keys) = self.infer_place_load(
6843+
let (resolved, keys) = self.record_place_load(
67926844
PlaceExprRef::from(&place_expr),
67936845
ast::ExprRef::Attribute(attribute),
67946846
);
@@ -8338,7 +8390,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
83388390
// If `value` is a valid reference, we attempt type narrowing by assignment.
83398391
if !value_ty.is_unknown() {
83408392
if let Some(expr) = PlaceExpr::try_from_expr(subscript) {
8341-
let (place, keys) = self.infer_place_load(
8393+
let (place, keys) = self.record_place_load(
83428394
PlaceExprRef::from(&expr),
83438395
ast::ExprRef::Subscript(subscript),
83448396
);
@@ -8877,6 +8929,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
88778929
declarations,
88788930
deferred,
88798931
cycle_fallback,
8932+
all_definitely_bound: all_bound,
88808933

88818934
// builder only state
88828935
deferred_state: _,
@@ -8912,6 +8965,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
89128965
bindings: bindings.into_boxed_slice(),
89138966
diagnostics,
89148967
cycle_fallback,
8968+
all_definitely_bound: all_bound,
89158969
})
89168970
});
89178971

@@ -8936,6 +8990,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
89368990
declarations,
89378991
deferred,
89388992
cycle_fallback,
8993+
all_definitely_bound: _,
89398994

89408995
// builder only state
89418996
deferred_state: _,
@@ -8998,6 +9053,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
89989053
deferred: _,
89999054
bindings: _,
90009055
declarations: _,
9056+
all_definitely_bound: _,
90019057

90029058
// Builder only state
90039059
deferred_state: _,
@@ -11092,6 +11148,80 @@ mod tests {
1109211148
assert_diagnostic_messages(&diagnostics, expected);
1109311149
}
1109411150

11151+
#[test]
11152+
fn analyze_cycles_gridout() {
11153+
let mut db = setup_db();
11154+
let filename = "src/gridout.py";
11155+
db.write_dedented(
11156+
filename,
11157+
r#"
11158+
EMPTY = b""
11159+
11160+
11161+
class GridOut:
11162+
def __init__(self: "GridOut") -> None:
11163+
self._buffer_pos = 0
11164+
self._buffer = b""
11165+
11166+
def readchunk(self: "GridOut") -> bytes:
11167+
if not len(self._buffer) - self._buffer_pos:
11168+
raise Exception("truncated chunk")
11169+
self._buffer_pos = 0
11170+
return EMPTY
11171+
11172+
def _read_size_or_line(self: "GridOut", size: int = -1) -> bytes:
11173+
if size > self._position:
11174+
size = self._position
11175+
if size == 0:
11176+
return bytes()
11177+
11178+
received = 0
11179+
needed = size - received
11180+
while received < size:
11181+
if self._buffer:
11182+
buf = self._buffer
11183+
chunk_start = self._buffer_pos
11184+
chunk_data = buf[self._buffer_pos :]
11185+
self._buffer = EMPTY
11186+
else:
11187+
buf = self.readchunk()
11188+
chunk_start = 0
11189+
chunk_data = buf
11190+
11191+
needed = buf.find(EMPTY, chunk_start, chunk_start + needed)
11192+
if len(chunk_data) > needed:
11193+
self._buffer = buf
11194+
self._buffer_pos = chunk_start + needed
11195+
self._position -= len(self._buffer) - self._buffer_pos
11196+
11197+
return b""
11198+
"#,
11199+
)
11200+
.unwrap();
11201+
11202+
db.clear_salsa_events();
11203+
assert_file_diagnostics(&db, filename, &[]);
11204+
let events = db.take_salsa_events();
11205+
let cycles = salsa::attach(&db, || {
11206+
events
11207+
.iter()
11208+
.filter_map(|event| {
11209+
if let salsa::EventKind::WillIterateCycle {
11210+
database_key,
11211+
iteration_count,
11212+
fell_back: _,
11213+
} = event.kind
11214+
{
11215+
Some(format!("{database_key:?}, {iteration_count:?}"))
11216+
} else {
11217+
None
11218+
}
11219+
})
11220+
.collect::<Vec<_>>()
11221+
});
11222+
assert_eq!(cycles.len(), 651);
11223+
}
11224+
1109511225
#[test]
1109611226
fn not_literal_string() -> anyhow::Result<()> {
1109711227
let mut db = setup_db();

0 commit comments

Comments
 (0)