@@ -337,6 +337,45 @@ fn single_expression_cycle_initial<'db>(
337337 Type::Never
338338}
339339
340+ /// Returns the statically-known truthiness of a given expression.
341+ ///
342+ /// Returns [`Truthiness::Ambiguous`] in case any non-definitely bound places
343+ /// were encountered while inferring the type of the expression.
344+ #[salsa::tracked(cycle_fn=static_expression_truthiness_cycle_recover, cycle_initial=static_expression_truthiness_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)]
345+ pub(crate) fn static_expression_truthiness<'db>(
346+ db: &'db dyn Db,
347+ expression: Expression<'db>,
348+ ) -> Truthiness {
349+ let inference = infer_expression_types(db, expression);
350+
351+ if !inference.all_places_definitely_bound() {
352+ return Truthiness::Ambiguous;
353+ }
354+
355+ let file = expression.file(db);
356+ let module = parsed_module(db, file).load(db);
357+ let node = expression.node_ref(db, &module);
358+
359+ inference.expression_type(node).bool(db)
360+ }
361+
362+ #[expect(clippy::trivially_copy_pass_by_ref)]
363+ fn static_expression_truthiness_cycle_recover<'db>(
364+ _db: &'db dyn Db,
365+ _value: &Truthiness,
366+ _count: u32,
367+ _expression: Expression<'db>,
368+ ) -> salsa::CycleRecoveryAction<Truthiness> {
369+ salsa::CycleRecoveryAction::Iterate
370+ }
371+
372+ fn static_expression_truthiness_cycle_initial<'db>(
373+ _db: &'db dyn Db,
374+ _expression: Expression<'db>,
375+ ) -> Truthiness {
376+ Truthiness::Ambiguous
377+ }
378+
340379/// Infer the types for an [`Unpack`] operation.
341380///
342381/// This infers the expression type and performs structural match against the target expression
@@ -657,6 +696,9 @@ struct ExpressionInferenceExtra<'db> {
657696 ///
658697 /// Falls back to `Type::Never` if an expression is missing.
659698 cycle_fallback: bool,
699+
700+ /// `true` if all places in this expression are definitely bound
701+ all_definitely_bound: bool,
660702}
661703
662704impl<'db> ExpressionInference<'db> {
@@ -665,6 +707,7 @@ impl<'db> ExpressionInference<'db> {
665707 Self {
666708 extra: Some(Box::new(ExpressionInferenceExtra {
667709 cycle_fallback: true,
710+ all_definitely_bound: true,
668711 ..ExpressionInferenceExtra::default()
669712 })),
670713 expressions: FxHashMap::default(),
@@ -698,6 +741,14 @@ impl<'db> ExpressionInference<'db> {
698741 fn fallback_type(&self) -> Option<Type<'db>> {
699742 self.is_cycle_callback().then_some(Type::Never)
700743 }
744+
745+ /// Returns true if all places in this expression are definitely bound.
746+ pub(crate) fn all_places_definitely_bound(&self) -> bool {
747+ self.extra
748+ .as_ref()
749+ .map(|e| e.all_definitely_bound)
750+ .unwrap_or(true)
751+ }
701752}
702753
703754/// Whether the intersection type is on the left or right side of the comparison.
@@ -847,6 +898,9 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
847898 ///
848899 /// This is used only when constructing a cycle-recovery `TypeInference`.
849900 cycle_fallback: bool,
901+
902+ /// `true` if all places in this expression are definitely bound
903+ all_definitely_bound: bool,
850904}
851905
852906impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
@@ -880,6 +934,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
880934 deferred: VecSet::default(),
881935 undecorated_type: None,
882936 cycle_fallback: false,
937+ all_definitely_bound: true,
883938 }
884939 }
885940
@@ -6614,7 +6669,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
66146669 let (resolved, constraint_keys) =
66156670 self.infer_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node));
66166671
6617- resolved
6672+ let resolved_after_fallback = resolved
66186673 // Not found in the module's explicitly declared global symbols?
66196674 // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc.
66206675 // These are looked up as attributes on `types.ModuleType`.
@@ -6650,8 +6705,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
66506705 } else {
66516706 Place::Unbound.into()
66526707 }
6653- } )
6654- . unwrap_with_diagnostic ( |lookup_error| match lookup_error {
6708+ });
6709+
6710+ if !resolved_after_fallback.place.is_definitely_bound() {
6711+ self.all_definitely_bound = false;
6712+ }
6713+
6714+ let ty =
6715+ resolved_after_fallback.unwrap_with_diagnostic(|lookup_error| match lookup_error {
66556716 LookupError::Unbound(qualifiers) => {
66566717 self.report_unresolved_reference(name_node);
66576718 TypeAndQualifiers::new(Type::unknown(), qualifiers)
@@ -6662,8 +6723,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
66626723 }
66636724 type_when_bound
66646725 }
6665- } )
6666- . inner_type ( )
6726+ });
6727+
6728+ ty.inner_type()
66676729 }
66686730
66696731 fn infer_local_place_load(
@@ -7093,7 +7155,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
70937155 }
70947156
70957157 fn narrow_expr_with_applicable_constraints<'r>(
7096- & self ,
7158+ &mut self,
70977159 target: impl Into<ast::ExprRef<'r>>,
70987160 target_ty: Type<'db>,
70997161 constraint_keys: &[(FileScopeId, ConstraintKey)],
@@ -7136,11 +7198,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
71367198 assigned_type = Some(ty);
71377199 }
71387200 }
7201+ let fallback_place = value_type.member(db, &attr.id);
7202+ if !fallback_place.place.is_definitely_bound()
7203+ || fallback_place
7204+ .qualifiers
7205+ .contains(TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE)
7206+ {
7207+ self.all_definitely_bound = false;
7208+ }
71397209
7140- let resolved_type = value_type
7141- . member ( db , & attr . id )
7142- . map_type ( |ty| self . narrow_expr_with_applicable_constraints ( attribute, ty, & constraint_keys) )
7143- . unwrap_with_diagnostic ( |lookup_error| match lookup_error {
7210+ let resolved_type =
7211+ fallback_place.map_type(|ty| {
7212+ self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys)
7213+ }) .unwrap_with_diagnostic(|lookup_error| match lookup_error {
71447214 LookupError::Unbound(_) => {
71457215 let report_unresolved_attribute = self.is_reachable(attribute);
71467216
@@ -9248,6 +9318,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
92489318 declarations,
92499319 deferred,
92509320 cycle_fallback,
9321+ all_definitely_bound,
92519322
92529323 // Ignored; only relevant to definition regions
92539324 undecorated_type: _,
@@ -9274,7 +9345,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
92749345 );
92759346
92769347 let extra =
9277- ( cycle_fallback || !bindings. is_empty ( ) || !diagnostics. is_empty ( ) ) . then ( || {
9348+ (cycle_fallback || !bindings.is_empty() || !diagnostics.is_empty() || !all_definitely_bound ).then(|| {
92789349 if bindings.len() > 20 {
92799350 tracing::debug!(
92809351 "Inferred expression region `{:?}` contains {} bindings. Lookups by linear scan might be slow.",
@@ -9287,6 +9358,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
92879358 bindings: bindings.into_boxed_slice(),
92889359 diagnostics,
92899360 cycle_fallback,
9361+ all_definitely_bound,
92909362 })
92919363 });
92929364
@@ -9312,7 +9384,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
93129384 deferred,
93139385 cycle_fallback,
93149386 undecorated_type,
9315-
9387+ all_definitely_bound: _,
93169388 // builder only state
93179389 typevar_binding_context: _,
93189390 deferred_state: _,
@@ -9379,6 +9451,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
93799451 deferred: _,
93809452 bindings: _,
93819453 declarations: _,
9454+ all_definitely_bound: _,
93829455
93839456 // Ignored; only relevant to definition regions
93849457 undecorated_type: _,
0 commit comments