astral-sh/ruff · critical

every BDD constraint should have a source-order entry

Error message

every BDD constraint should have a source-order entry

What it means

Internal assertion in `Bdd::path_assignments` (crates/ty_python_semantic/src/types/constraints.rs:4649). After collecting every unique constraint reachable from the node (`for_each_unique_constraint`) and building the sidecar with `storage.calculate_source_orders(source_order)`, the code sorts constraints by their sidecar index. The `expect` enforces the contract documented in the adjacent comment (constraints.rs:4641-4645): every constraint in the TDD must appear in the source-order sidecar, and any operation that introduces new constraints must preserve their source orders instead of inventing an order — `PathAssignments` seeds its insertion-ordered discovered-constraint map from this sorted list and uses that order to build non-commutative sequent pairs. A lookup miss means a BDD operation added an untracked constraint; sorting on TDD traversal order would silently change inference and lose gradual constraints, so ty panics instead.

Source

Thrown at crates/ty_python_semantic/src/types/constraints.rs:4713

        let self_ordering = self_interior.constraint.ordering();
        let other_interior = storage.interior_node_data(other.node());
        let other_ordering = other_interior.constraint.ordering();
        let result = match self_ordering.cmp(&other_ordering) {
            Ordering::Equal => {
                let if_true = self_interior.if_true.or(storage, other_interior.if_true);
                let if_uncertain = self_interior
                    .if_uncertain
                    .or(storage, other_interior.if_uncertain);
                let if_false = self_interior.if_false.or(storage, other_interior.if_false);
                NodeId::with_uncertain(
                    storage,
                    self_interior.constraint,
                    if_true,
                    if_uncertain,
                    if_false,
                )
            }
            // This is from Frisch's original description of TDDs. If self < other, we check self
            // first. Instead of distributing other into the if_true and if_false branches, we
            // "park" it in the if_uncertain branch. That causes us to only evaluate other "lazily"
            // when needed.
            Ordering::Less => {
                let if_uncertain = self_interior.if_uncertain.or(storage, other.node());
                NodeId::with_uncertain(
                    storage,
                    self_interior.constraint,
                    self_interior.if_true,
                    if_uncertain,
                    self_interior.if_false,
                )
            }
            // Ditto above but for the other variable ordering
            Ordering::Greater => {
                let if_uncertain = self.node().or(storage, other_interior.if_uncertain);
                NodeId::with_uncertain(
                    storage,

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. As a ty user: capture the panic with `RUST_BACKTRACE=1 ty check <file>`, reduce to a minimal snippet, and open an issue in the ruff repo with backtrace, version, and reproducer.
  2. Downgrade or pin the previously working ty version while the fix lands.
  3. As a ty developer: locate the operation that introduced the untracked constraint and make it propagate the original `SourceOrderId` or extend the sidecar via `calculate_source_orders` — never sort by TDD traversal order (the comment at constraints.rs:4641-4645 explains why).
  4. Cover the fix with a minimized mdtest and run the semantic crate's tests: `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic`.
Defensive patterns

Strategy: try-catch

Try / catch

// Embedders: same per-file panic isolation as other ty ICEs
use std::panic::{catch_unwind, AssertUnwindSafe};

let checked = catch_unwind(AssertUnwindSafe(|| analyze(db, file)));
if checked.is_err() { log::error!("ty ICE in path_assignments for {file}"); }

Prevention

When it happens

Trigger: Any inference path reaching `path_assignments` (e.g. the `abstract()` pass at constraints.rs:4622 or solution selection) after an operation inserted a `ConstraintId` into the diagram that `calculate_source_orders` never saw — for instance a `Disposition::Remove` or `ite` rewrite substituting a derived constraint while keeping only one branch's source order. Fires on code with complex constraint sets: recursive generics, bounded PEP 695 typevars, overloaded calls.

Common situations: Seen by ty contributors after refactoring BDD operations (restriction, exists-abstraction, ite, or) without threading source orders through; or by nightly users checking large generic codebases after upgrading across a solver refactor. Not related to configuration, environment, or the Python code's correctness.

Related errors


AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20). Data as JSON: /api/errors/f0d9a9180fb98edc. Report an issue: GitHub.