Skip to main content

ty_python_core/use_def/
place_state.rs

1//! Track live bindings per place, applicable constraints per binding, and live declarations.
2//!
3//! These data structures operate entirely on scope-local newtype-indices for definitions and
4//! constraints, referring to their location in the `all_definitions` and `all_constraints`
5//! indexvecs in [`super::UseDefMapBuilder`].
6//!
7//! We need to track arbitrary associations between bindings and constraints, not just a single set
8//! of currently dominating constraints (where "dominating" means "control flow must have passed
9//! through it to reach this point"), because we can have dominating constraints that apply to some
10//! bindings but not others, as in this code:
11//!
12//! ```python
13//! x = 1 if flag else None
14//! if x is not None:
15//!     if flag2:
16//!         x = 2 if flag else None
17//!     x
18//! ```
19//!
20//! The `x is not None` constraint dominates the final use of `x`, but it applies only to the first
21//! binding of `x`, not the second, so `None` is a possible value for `x`.
22//!
23//! And we can't just track, for each binding, an index into a list of dominating constraints,
24//! either, because we can have bindings which are still visible, but subject to constraints that
25//! are no longer dominating, as in this code:
26//!
27//! ```python
28//! x = 0
29//! if flag1:
30//!     x = 1 if flag2 else None
31//!     assert x is not None
32//! x
33//! ```
34//!
35//! From the point of view of the final use of `x`, the `x is not None` constraint no longer
36//! dominates, but it does dominate the `x = 1 if flag2 else None` binding, so we have to keep
37//! track of that.
38//!
39//! The data structures use `IndexVec` arenas to store all data compactly and contiguously, while
40//! supporting very cheap clones.
41//!
42//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
43//! similar to tracking live bindings.
44
45use itertools::{EitherOrBoth, Itertools};
46use ruff_index::newtype_index;
47use smallvec::{SmallVec, smallvec};
48
49use crate::ReachabilityConstraintsBuilder;
50use crate::narrowing_constraints::{NarrowingConstraintsBuilder, ScopedNarrowingConstraint};
51use crate::reachability_constraints::ScopedReachabilityConstraintId;
52
53/// An index into a scope's use-def history. A combined definition can have separate declaration
54/// and binding entries when they take effect at different points in control flow.
55#[newtype_index]
56#[derive(Ord, PartialOrd, get_size2::GetSize)]
57pub struct ScopedDefinitionId;
58
59impl ScopedDefinitionId {
60    /// A special ID that is used to describe an implicit start-of-scope state. When
61    /// we see that this definition is live, we know that the place is (possibly)
62    /// unbound or undeclared at a given usage site.
63    /// When creating a use-def-map builder, we always add an empty `DefinitionState::Undefined` definition
64    /// at index 0, so this ID is always present.
65    pub(crate) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0);
66
67    pub(crate) fn is_unbound(self) -> bool {
68        self == Self::UNBOUND
69    }
70}
71
72/// Live declarations for a single place at some point in control flow, with their
73/// corresponding reachability constraints.
74#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, get_size2::GetSize)]
75pub(super) struct Declarations {
76    /// A list of live declarations for this place, sorted by their `ScopedDefinitionId`
77    live_declarations: SmallVec<[LiveDeclaration; 2]>,
78}
79
80/// One of the live declarations for a single place at some point in control flow.
81#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
82pub(super) struct LiveDeclaration {
83    pub(super) declaration: ScopedDefinitionId,
84    pub(super) reachability_constraint: ScopedReachabilityConstraintId,
85}
86
87pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>;
88
89/// What happens to any preexisting definitions when a new binding of the same place is added.
90/// `AreShadowed` is how normal assignments behave, but we model some features (loop headers,
91/// `nonlocal` writes from nested scopes) as "synthetic" bindings that don't shadow other bindings.
92#[derive(Clone, Copy, Debug)]
93pub(crate) enum PreviousDefinitions {
94    AreShadowed,
95    AreKept,
96}
97
98/// What will happen to a definition if/when a when a new binding of the same place is added later.
99/// `ShadowThisOne` is how normal assignments behave, and it's also how some "synthetic" bindings
100/// behave (loop headers), but there are other synthetic bindings (nested `nonlocal` writes) that
101/// cannot be shadowed.
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
103pub(crate) enum FutureDefinitions {
104    ShadowThisOne,
105    DontShadowThisOne,
106}
107
108impl PreviousDefinitions {
109    fn are_shadowed(self) -> bool {
110        matches!(self, PreviousDefinitions::AreShadowed)
111    }
112}
113
114impl Declarations {
115    pub(super) fn undeclared_reachability_constraint(
116        &self,
117    ) -> Option<ScopedReachabilityConstraintId> {
118        let [
119            LiveDeclaration {
120                declaration: ScopedDefinitionId::UNBOUND,
121                reachability_constraint,
122            },
123        ] = self.live_declarations.as_slice()
124        else {
125            return None;
126        };
127        Some(*reachability_constraint)
128    }
129
130    pub(super) fn is_always_undeclared(&self) -> bool {
131        self.undeclared_reachability_constraint()
132            == Some(ScopedReachabilityConstraintId::ALWAYS_TRUE)
133    }
134
135    pub(super) fn undeclared(reachability_constraint: ScopedReachabilityConstraintId) -> Self {
136        let initial_declaration = LiveDeclaration {
137            declaration: ScopedDefinitionId::UNBOUND,
138            reachability_constraint,
139        };
140        Self {
141            live_declarations: smallvec![initial_declaration],
142        }
143    }
144
145    /// Record a newly-encountered declaration for this place.
146    pub(super) fn record_declaration(
147        &mut self,
148        declaration: ScopedDefinitionId,
149        reachability_constraint: ScopedReachabilityConstraintId,
150        previous_definitions: PreviousDefinitions,
151    ) {
152        if previous_definitions.are_shadowed() {
153            // The new declaration replaces all previous live declaration in this path.
154            self.live_declarations.clear();
155        }
156        self.live_declarations.push(LiveDeclaration {
157            declaration,
158            reachability_constraint,
159        });
160    }
161
162    /// Add given reachability constraint to all live declarations.
163    fn record_reachability_constraint(
164        &mut self,
165        reachability_constraints: &mut ReachabilityConstraintsBuilder,
166        constraint: ScopedReachabilityConstraintId,
167    ) {
168        for declaration in &mut self.live_declarations {
169            declaration.reachability_constraint = reachability_constraints
170                .add_and_constraint(declaration.reachability_constraint, constraint);
171        }
172    }
173
174    /// Return an iterator over live declarations for this place.
175    pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> {
176        self.live_declarations.iter()
177    }
178
179    pub(super) fn as_slice(&self) -> &[LiveDeclaration] {
180        &self.live_declarations
181    }
182
183    fn merge(&mut self, b: Self, reachability_constraints: &mut ReachabilityConstraintsBuilder) {
184        let a = std::mem::take(self);
185
186        // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
187        // the merged `live_declarations` vec remains sorted. If a definition is found in both `a`
188        // and `b`, we combine its reachability constraints. If a definition is found in only one
189        // path, it is used as-is.
190        let a = a.live_declarations.into_iter();
191        let b = b.live_declarations.into_iter();
192        for zipped in a.merge_join_by(b, |a, b| a.declaration.cmp(&b.declaration)) {
193            match zipped {
194                EitherOrBoth::Both(a, b) => {
195                    let reachability_constraint = reachability_constraints
196                        .add_or_constraint(a.reachability_constraint, b.reachability_constraint);
197                    self.live_declarations.push(LiveDeclaration {
198                        declaration: a.declaration,
199                        reachability_constraint,
200                    });
201                }
202
203                EitherOrBoth::Left(declaration) | EitherOrBoth::Right(declaration) => {
204                    self.live_declarations.push(declaration);
205                }
206            }
207        }
208    }
209}
210
211/// A snapshot of a place state that can be used to resolve a reference in a nested scope.
212/// If there are bindings in a (non-class) scope, they are stored in `Bindings`.
213/// Even if it's a class scope (class variables are not visible to nested scopes) or there are no
214/// bindings, the current narrowing constraint is necessary for narrowing, so it's stored in
215/// `Constraint`.
216#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
217pub(super) enum EnclosingSnapshot {
218    Constraint(ScopedNarrowingConstraint),
219    Bindings(Bindings),
220}
221
222/// Live bindings for a single place at some point in control flow. Each live binding comes
223/// with a set of narrowing constraints and a reachability constraint.
224#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, get_size2::GetSize)]
225pub(super) struct Bindings {
226    /// The narrowing constraint applicable to the "unbound" binding, if we need access to it even
227    /// when it's not visible. This happens in class scopes, where local name bindings are not visible
228    /// to nested scopes, but we still need to know what narrowing constraints were applied to the
229    /// "unbound" binding.
230    unbound_narrowing_constraint: Option<ScopedNarrowingConstraint>,
231    /// A list of live bindings for this place, sorted by their `ScopedDefinitionId`
232    live_bindings: SmallVec<[LiveBinding; 2]>,
233}
234
235impl Bindings {
236    pub(super) fn is_always_unbound(&self) -> bool {
237        let [binding] = self.live_bindings.as_slice() else {
238            return false;
239        };
240        self.unbound_narrowing_constraint.is_none()
241            && binding.binding() == ScopedDefinitionId::UNBOUND
242            && binding.narrowing_constraint == ScopedNarrowingConstraint::ALWAYS_TRUE
243            && binding.reachability_constraint == ScopedReachabilityConstraintId::ALWAYS_TRUE
244            && binding.can_be_shadowed() == FutureDefinitions::ShadowThisOne
245    }
246
247    pub(super) fn unbound_narrowing_constraint(&self) -> ScopedNarrowingConstraint {
248        self.unbound_narrowing_constraint
249            .unwrap_or(self.live_bindings[0].narrowing_constraint)
250    }
251
252    pub(super) fn finish(
253        &mut self,
254        narrowing_constraints: &mut NarrowingConstraintsBuilder,
255        reachability_constraints: &mut ReachabilityConstraintsBuilder,
256    ) {
257        self.live_bindings.shrink_to_fit();
258        for binding in &self.live_bindings {
259            reachability_constraints.mark_used(binding.reachability_constraint);
260            narrowing_constraints.mark_used(binding.narrowing_constraint);
261        }
262    }
263}
264
265/// One of the live bindings for a single place at some point in control flow.
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
267pub struct LiveBinding {
268    binding: PackedDefinitionId,
269    narrowing_constraint: ScopedNarrowingConstraint,
270    reachability_constraint: ScopedReachabilityConstraintId,
271}
272
273#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
274struct PackedDefinitionId(u32);
275
276impl PackedDefinitionId {
277    // Scope-local definition IDs cannot practically use the high bit, so retain the shadowing
278    // policy there instead of adding a byte plus padding to every `LiveBinding`.
279    const DONT_SHADOW: u32 = 1 << 31;
280    const DEFINITION_MASK: u32 = !Self::DONT_SHADOW;
281
282    fn new(binding: ScopedDefinitionId, can_be_shadowed: FutureDefinitions) -> Self {
283        let binding = binding.as_u32();
284        assert_eq!(
285            binding & Self::DONT_SHADOW,
286            0,
287            "scopes cannot contain more than 2^31 definitions"
288        );
289        Self(
290            binding
291                | match can_be_shadowed {
292                    FutureDefinitions::ShadowThisOne => 0,
293                    FutureDefinitions::DontShadowThisOne => Self::DONT_SHADOW,
294                },
295        )
296    }
297
298    const fn definition(self) -> ScopedDefinitionId {
299        ScopedDefinitionId::from_u32(self.0 & Self::DEFINITION_MASK)
300    }
301
302    const fn can_be_shadowed(self) -> FutureDefinitions {
303        if self.0 & Self::DONT_SHADOW == 0 {
304            FutureDefinitions::ShadowThisOne
305        } else {
306            FutureDefinitions::DontShadowThisOne
307        }
308    }
309}
310
311impl LiveBinding {
312    fn new(
313        binding: ScopedDefinitionId,
314        narrowing_constraint: ScopedNarrowingConstraint,
315        reachability_constraint: ScopedReachabilityConstraintId,
316        can_be_shadowed: FutureDefinitions,
317    ) -> Self {
318        Self {
319            binding: PackedDefinitionId::new(binding, can_be_shadowed),
320            narrowing_constraint,
321            reachability_constraint,
322        }
323    }
324
325    pub const fn binding(&self) -> ScopedDefinitionId {
326        self.binding.definition()
327    }
328
329    pub const fn narrowing_constraint(&self) -> ScopedNarrowingConstraint {
330        self.narrowing_constraint
331    }
332
333    pub const fn reachability_constraint(&self) -> ScopedReachabilityConstraintId {
334        self.reachability_constraint
335    }
336
337    const fn can_be_shadowed(&self) -> FutureDefinitions {
338        self.binding.can_be_shadowed()
339    }
340}
341
342static_assertions::assert_eq_size!(LiveBinding, [u32; 3]);
343
344pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>;
345
346impl Bindings {
347    pub(super) fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self {
348        let initial_binding = LiveBinding::new(
349            ScopedDefinitionId::UNBOUND,
350            ScopedNarrowingConstraint::ALWAYS_TRUE,
351            reachability_constraint,
352            FutureDefinitions::ShadowThisOne,
353        );
354        Self {
355            unbound_narrowing_constraint: None,
356            live_bindings: smallvec![initial_binding],
357        }
358    }
359
360    /// Record a newly-encountered binding for this place.
361    pub(super) fn record_binding(
362        &mut self,
363        binding: ScopedDefinitionId,
364        reachability_constraint: ScopedReachabilityConstraintId,
365        is_class_scope: bool,
366        is_place_name: bool,
367        previous_definitions: PreviousDefinitions,
368        can_be_shadowed: FutureDefinitions,
369    ) {
370        // If we are in a class scope, and the unbound name binding was previously visible, but we will
371        // now replace it, record the narrowing constraints on it:
372        if is_class_scope && is_place_name && self.live_bindings[0].binding().is_unbound() {
373            self.unbound_narrowing_constraint = Some(self.live_bindings[0].narrowing_constraint);
374        }
375        // If the new binding is a shadowing type, it replaces previous live bindings in this path
376        // (unless they're marked as not shadowable), and has no constraints.
377        if previous_definitions.are_shadowed() {
378            self.live_bindings
379                .retain(|b| b.can_be_shadowed() == FutureDefinitions::DontShadowThisOne);
380        }
381        self.live_bindings.push(LiveBinding::new(
382            binding,
383            ScopedNarrowingConstraint::ALWAYS_TRUE,
384            reachability_constraint,
385            can_be_shadowed,
386        ));
387    }
388
389    /// Add given constraint to all live bindings.
390    fn record_narrowing_constraint(
391        &mut self,
392        narrowing_constraints: &mut NarrowingConstraintsBuilder,
393        constraint: ScopedNarrowingConstraint,
394    ) {
395        for binding in &mut self.live_bindings {
396            binding.narrowing_constraint =
397                narrowing_constraints.add_and_constraint(binding.narrowing_constraint, constraint);
398        }
399    }
400
401    /// Add given reachability constraint to all live bindings.
402    fn record_reachability_constraint(
403        &mut self,
404        reachability_constraints: &mut ReachabilityConstraintsBuilder,
405        constraint: ScopedReachabilityConstraintId,
406    ) {
407        for binding in &mut self.live_bindings {
408            binding.reachability_constraint = reachability_constraints
409                .add_and_constraint(binding.reachability_constraint, constraint);
410        }
411    }
412
413    /// Iterate over currently live bindings for this place
414    pub(super) fn iter(&self) -> LiveBindingsIterator<'_> {
415        self.live_bindings.iter()
416    }
417
418    pub(super) fn as_slice(&self) -> &[LiveBinding] {
419        &self.live_bindings
420    }
421
422    pub(super) fn merge(
423        &mut self,
424        b: Self,
425        narrowing_constraints: &mut NarrowingConstraintsBuilder,
426        reachability_constraints: &mut ReachabilityConstraintsBuilder,
427    ) {
428        let a = std::mem::take(self);
429
430        if let Some((a, b)) = a
431            .unbound_narrowing_constraint
432            .zip(b.unbound_narrowing_constraint)
433        {
434            self.unbound_narrowing_constraint = Some(narrowing_constraints.add_or_constraint(a, b));
435        }
436
437        // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
438        // the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and
439        // `b`, we combine its boolean narrowing constraints and its ternary reachability
440        // constraints. If a definition is found in only one path, it is used as-is.
441        let a = a.live_bindings.into_iter();
442        let b = b.live_bindings.into_iter();
443        for zipped in a.merge_join_by(b, |a, b| a.binding().cmp(&b.binding())) {
444            match zipped {
445                EitherOrBoth::Both(a, b) => {
446                    // If the same definition is visible through both paths, we OR the narrowing
447                    // constraints: the type should be narrowed by whichever path was taken.
448                    let narrowing_constraint = narrowing_constraints
449                        .add_or_constraint(a.narrowing_constraint, b.narrowing_constraint);
450
451                    // For reachability constraints, we also merge using a ternary OR operation:
452                    let reachability_constraint = reachability_constraints
453                        .add_or_constraint(a.reachability_constraint, b.reachability_constraint);
454
455                    debug_assert_eq!(a.can_be_shadowed(), b.can_be_shadowed());
456                    self.live_bindings.push(LiveBinding::new(
457                        a.binding(),
458                        narrowing_constraint,
459                        reachability_constraint,
460                        a.can_be_shadowed(),
461                    ));
462                }
463
464                EitherOrBoth::Left(binding) | EitherOrBoth::Right(binding) => {
465                    self.live_bindings.push(binding);
466                }
467            }
468        }
469    }
470}
471
472#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
473pub(crate) struct PlaceState {
474    declarations: Declarations,
475    bindings: Bindings,
476}
477
478impl PlaceState {
479    /// Return a new [`PlaceState`] representing an unbound, undeclared place.
480    pub(super) fn undefined(reachability: ScopedReachabilityConstraintId) -> Self {
481        Self {
482            declarations: Declarations::undeclared(reachability),
483            bindings: Bindings::unbound(reachability),
484        }
485    }
486
487    /// Record a newly-encountered binding for this place.
488    pub(super) fn record_binding(
489        &mut self,
490        binding_id: ScopedDefinitionId,
491        reachability_constraint: ScopedReachabilityConstraintId,
492        is_class_scope: bool,
493        is_place_name: bool,
494        previous_definitions: PreviousDefinitions,
495        can_be_shadowed: FutureDefinitions,
496    ) {
497        debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
498        self.bindings.record_binding(
499            binding_id,
500            reachability_constraint,
501            is_class_scope,
502            is_place_name,
503            previous_definitions,
504            can_be_shadowed,
505        );
506    }
507
508    /// Add given constraint to all live bindings.
509    pub(super) fn record_narrowing_constraint(
510        &mut self,
511        narrowing_constraints: &mut NarrowingConstraintsBuilder,
512        constraint: ScopedNarrowingConstraint,
513    ) {
514        self.bindings
515            .record_narrowing_constraint(narrowing_constraints, constraint);
516    }
517
518    /// Add the given constraint to live bindings that were also present at an earlier use.
519    pub(super) fn record_narrowing_constraint_for_bindings_at_use(
520        &mut self,
521        narrowing_constraints: &mut NarrowingConstraintsBuilder,
522        constraint: ScopedNarrowingConstraint,
523        bindings_at_use: &Bindings,
524    ) {
525        for binding in &mut self.bindings.live_bindings {
526            if bindings_at_use
527                .iter()
528                .any(|binding_at_use| binding_at_use.binding() == binding.binding())
529            {
530                binding.narrowing_constraint = narrowing_constraints
531                    .add_and_constraint(binding.narrowing_constraint, constraint);
532            }
533        }
534    }
535
536    /// Add the given constraint to live bindings selected by definition ID.
537    pub(super) fn record_narrowing_constraint_for_bindings(
538        &mut self,
539        narrowing_constraints: &mut NarrowingConstraintsBuilder,
540        constraint: ScopedNarrowingConstraint,
541        bindings: &[ScopedDefinitionId],
542    ) {
543        for binding in &mut self.bindings.live_bindings {
544            if bindings.contains(&binding.binding()) {
545                binding.narrowing_constraint = narrowing_constraints
546                    .add_and_constraint(binding.narrowing_constraint, constraint);
547            }
548        }
549    }
550
551    /// Add given reachability constraint to all live bindings.
552    pub(super) fn record_reachability_constraint(
553        &mut self,
554        reachability_constraints: &mut ReachabilityConstraintsBuilder,
555        constraint: ScopedReachabilityConstraintId,
556    ) {
557        self.bindings
558            .record_reachability_constraint(reachability_constraints, constraint);
559        self.declarations
560            .record_reachability_constraint(reachability_constraints, constraint);
561    }
562
563    /// Record a newly-encountered declaration of this place.
564    pub(super) fn record_declaration(
565        &mut self,
566        declaration_id: ScopedDefinitionId,
567        reachability_constraint: ScopedReachabilityConstraintId,
568    ) {
569        self.declarations.record_declaration(
570            declaration_id,
571            reachability_constraint,
572            PreviousDefinitions::AreShadowed,
573        );
574    }
575
576    /// Merge another [`PlaceState`] into this one.
577    pub(super) fn merge(
578        &mut self,
579        b: PlaceState,
580        narrowing_constraints: &mut NarrowingConstraintsBuilder,
581        reachability_constraints: &mut ReachabilityConstraintsBuilder,
582    ) {
583        self.bindings
584            .merge(b.bindings, narrowing_constraints, reachability_constraints);
585        self.declarations
586            .merge(b.declarations, reachability_constraints);
587    }
588
589    pub(super) fn bindings(&self) -> &Bindings {
590        &self.bindings
591    }
592
593    pub(super) fn declarations(&self) -> &Declarations {
594        &self.declarations
595    }
596
597    pub(super) fn into_parts(self) -> (Bindings, Declarations) {
598        (self.bindings, self.declarations)
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use ruff_index::Idx;
606
607    use crate::predicate::ScopedPredicateId;
608
609    #[track_caller]
610    fn assert_bindings(place: &PlaceState, expected: &[(u32, ScopedNarrowingConstraint)]) {
611        let actual: Vec<(u32, ScopedNarrowingConstraint)> = place
612            .bindings()
613            .iter()
614            .map(|live_binding| {
615                (
616                    live_binding.binding().as_u32(),
617                    live_binding.narrowing_constraint,
618                )
619            })
620            .collect();
621        assert_eq!(actual, expected);
622    }
623
624    #[track_caller]
625    fn assert_declarations(place: &PlaceState, expected: &[&str]) {
626        let actual = place
627            .declarations()
628            .iter()
629            .map(
630                |LiveDeclaration {
631                     declaration,
632                     reachability_constraint: _,
633                 }| {
634                    if *declaration == ScopedDefinitionId::UNBOUND {
635                        "undeclared".into()
636                    } else {
637                        declaration.as_u32().to_string()
638                    }
639                },
640            )
641            .collect::<Vec<_>>();
642        assert_eq!(actual, expected);
643    }
644
645    #[test]
646    fn unbound() {
647        let sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
648
649        assert_bindings(&sym, &[(0, ScopedNarrowingConstraint::ALWAYS_TRUE)]);
650    }
651
652    #[test]
653    fn with() {
654        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
655        sym.record_binding(
656            ScopedDefinitionId::from_u32(1),
657            ScopedReachabilityConstraintId::ALWAYS_TRUE,
658            false,
659            true,
660            PreviousDefinitions::AreShadowed,
661            FutureDefinitions::ShadowThisOne,
662        );
663
664        assert_bindings(&sym, &[(1, ScopedNarrowingConstraint::ALWAYS_TRUE)]);
665    }
666
667    #[test]
668    fn future_definitions_can_opt_out_of_shadowing() {
669        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
670        sym.record_binding(
671            ScopedDefinitionId::from_u32(1),
672            ScopedReachabilityConstraintId::ALWAYS_TRUE,
673            false,
674            true,
675            PreviousDefinitions::AreKept,
676            FutureDefinitions::DontShadowThisOne,
677        );
678        sym.record_binding(
679            ScopedDefinitionId::from_u32(2),
680            ScopedReachabilityConstraintId::ALWAYS_TRUE,
681            false,
682            true,
683            PreviousDefinitions::AreShadowed,
684            FutureDefinitions::ShadowThisOne,
685        );
686
687        assert_bindings(
688            &sym,
689            &[
690                (1, ScopedNarrowingConstraint::ALWAYS_TRUE),
691                (2, ScopedNarrowingConstraint::ALWAYS_TRUE),
692            ],
693        );
694
695        sym.record_binding(
696            ScopedDefinitionId::from_u32(3),
697            ScopedReachabilityConstraintId::ALWAYS_TRUE,
698            false,
699            true,
700            PreviousDefinitions::AreShadowed,
701            FutureDefinitions::ShadowThisOne,
702        );
703
704        assert_bindings(
705            &sym,
706            &[
707                (1, ScopedNarrowingConstraint::ALWAYS_TRUE),
708                (3, ScopedNarrowingConstraint::ALWAYS_TRUE),
709            ],
710        );
711    }
712
713    #[test]
714    fn record_constraint() {
715        let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
716        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
717        sym.record_binding(
718            ScopedDefinitionId::from_u32(1),
719            ScopedReachabilityConstraintId::ALWAYS_TRUE,
720            false,
721            true,
722            PreviousDefinitions::AreShadowed,
723            FutureDefinitions::ShadowThisOne,
724        );
725        let atom = narrowing_constraints.add_atom(ScopedPredicateId::new(0));
726        sym.record_narrowing_constraint(&mut narrowing_constraints, atom);
727
728        assert_bindings(&sym, &[(1, atom)]);
729    }
730
731    #[test]
732    fn merge() {
733        let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
734        let mut reachability_constraints = ReachabilityConstraintsBuilder::default();
735
736        // merging the same definition with the same constraint keeps the constraint
737        let mut sym1a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
738        sym1a.record_binding(
739            ScopedDefinitionId::from_u32(1),
740            ScopedReachabilityConstraintId::ALWAYS_TRUE,
741            false,
742            true,
743            PreviousDefinitions::AreShadowed,
744            FutureDefinitions::ShadowThisOne,
745        );
746        let atom0 = narrowing_constraints.add_atom(ScopedPredicateId::new(0));
747        sym1a.record_narrowing_constraint(&mut narrowing_constraints, atom0);
748
749        let mut sym1b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
750        sym1b.record_binding(
751            ScopedDefinitionId::from_u32(1),
752            ScopedReachabilityConstraintId::ALWAYS_TRUE,
753            false,
754            true,
755            PreviousDefinitions::AreShadowed,
756            FutureDefinitions::ShadowThisOne,
757        );
758        sym1b.record_narrowing_constraint(&mut narrowing_constraints, atom0);
759
760        sym1a.merge(
761            sym1b,
762            &mut narrowing_constraints,
763            &mut reachability_constraints,
764        );
765        let mut sym1 = sym1a;
766        // Same constraint on both sides → OR(atom0, atom0) = atom0
767        assert_bindings(&sym1, &[(1, atom0)]);
768
769        // merging the same definition with differing constraints produces OR (not empty)
770        let mut sym2a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
771        sym2a.record_binding(
772            ScopedDefinitionId::from_u32(2),
773            ScopedReachabilityConstraintId::ALWAYS_TRUE,
774            false,
775            true,
776            PreviousDefinitions::AreShadowed,
777            FutureDefinitions::ShadowThisOne,
778        );
779        let atom1 = narrowing_constraints.add_atom(ScopedPredicateId::new(1));
780        sym2a.record_narrowing_constraint(&mut narrowing_constraints, atom1);
781
782        let mut sym1b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
783        sym1b.record_binding(
784            ScopedDefinitionId::from_u32(2),
785            ScopedReachabilityConstraintId::ALWAYS_TRUE,
786            false,
787            true,
788            PreviousDefinitions::AreShadowed,
789            FutureDefinitions::ShadowThisOne,
790        );
791        let atom2 = narrowing_constraints.add_atom(ScopedPredicateId::new(2));
792        sym1b.record_narrowing_constraint(&mut narrowing_constraints, atom2);
793
794        sym2a.merge(
795            sym1b,
796            &mut narrowing_constraints,
797            &mut reachability_constraints,
798        );
799        let sym2 = sym2a;
800        // Different constraints: OR(atom1, atom2) produces a new TDD node (not a terminal)
801        let merged_constraint = sym2.bindings().iter().next().unwrap().narrowing_constraint;
802        assert_ne!(merged_constraint, ScopedNarrowingConstraint::ALWAYS_TRUE);
803        assert_ne!(merged_constraint, ScopedNarrowingConstraint::ALWAYS_FALSE);
804        assert_ne!(merged_constraint, atom1);
805        assert_ne!(merged_constraint, atom2);
806
807        // merging a constrained definition with unbound keeps both
808        let mut sym3a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
809        sym3a.record_binding(
810            ScopedDefinitionId::from_u32(3),
811            ScopedReachabilityConstraintId::ALWAYS_TRUE,
812            false,
813            true,
814            PreviousDefinitions::AreShadowed,
815            FutureDefinitions::ShadowThisOne,
816        );
817        let atom3 = narrowing_constraints.add_atom(ScopedPredicateId::new(3));
818        sym3a.record_narrowing_constraint(&mut narrowing_constraints, atom3);
819
820        let sym2b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
821
822        sym3a.merge(
823            sym2b,
824            &mut narrowing_constraints,
825            &mut reachability_constraints,
826        );
827        let sym3 = sym3a;
828        let bindings: Vec<_> = sym3
829            .bindings()
830            .iter()
831            .map(|b| (b.binding().as_u32(), b.narrowing_constraint))
832            .collect();
833        assert_eq!(bindings.len(), 2);
834        assert_eq!(bindings[0].0, 0); // unbound
835        assert_eq!(bindings[1].0, 3);
836        assert_eq!(bindings[1].1, atom3);
837
838        // merging different definitions keeps them each with their existing constraints
839        sym1.merge(
840            sym3,
841            &mut narrowing_constraints,
842            &mut reachability_constraints,
843        );
844        let sym = sym1;
845        let bindings: Vec<_> = sym
846            .bindings()
847            .iter()
848            .map(|b| (b.binding().as_u32(), b.narrowing_constraint))
849            .collect();
850        assert_eq!(bindings.len(), 3);
851        assert_eq!(bindings[0].0, 0); // unbound
852        assert_eq!(bindings[1].0, 1);
853        assert_eq!(bindings[1].1, atom0);
854        assert_eq!(bindings[2].0, 3);
855        assert_eq!(bindings[2].1, atom3);
856    }
857
858    #[test]
859    fn no_declaration() {
860        let sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
861
862        assert_declarations(&sym, &["undeclared"]);
863    }
864
865    #[test]
866    fn record_declaration() {
867        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
868        sym.record_declaration(
869            ScopedDefinitionId::from_u32(1),
870            ScopedReachabilityConstraintId::ALWAYS_TRUE,
871        );
872
873        assert_declarations(&sym, &["1"]);
874    }
875
876    #[test]
877    fn record_declaration_override() {
878        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
879        sym.record_declaration(
880            ScopedDefinitionId::from_u32(1),
881            ScopedReachabilityConstraintId::ALWAYS_TRUE,
882        );
883        sym.record_declaration(
884            ScopedDefinitionId::from_u32(2),
885            ScopedReachabilityConstraintId::ALWAYS_TRUE,
886        );
887
888        assert_declarations(&sym, &["2"]);
889    }
890
891    #[test]
892    fn record_declaration_merge() {
893        let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
894        let mut reachability_constraints = ReachabilityConstraintsBuilder::default();
895        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
896        sym.record_declaration(
897            ScopedDefinitionId::from_u32(1),
898            ScopedReachabilityConstraintId::ALWAYS_TRUE,
899        );
900
901        let mut sym2 = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
902        sym2.record_declaration(
903            ScopedDefinitionId::from_u32(2),
904            ScopedReachabilityConstraintId::ALWAYS_TRUE,
905        );
906
907        sym.merge(
908            sym2,
909            &mut narrowing_constraints,
910            &mut reachability_constraints,
911        );
912
913        assert_declarations(&sym, &["1", "2"]);
914    }
915
916    #[test]
917    fn record_declaration_merge_partial_undeclared() {
918        let mut narrowing_constraints = NarrowingConstraintsBuilder::default();
919        let mut reachability_constraints = ReachabilityConstraintsBuilder::default();
920        let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
921        sym.record_declaration(
922            ScopedDefinitionId::from_u32(1),
923            ScopedReachabilityConstraintId::ALWAYS_TRUE,
924        );
925
926        let sym2 = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE);
927
928        sym.merge(
929            sym2,
930            &mut narrowing_constraints,
931            &mut reachability_constraints,
932        );
933
934        assert_declarations(&sym, &["undeclared", "1"]);
935    }
936}