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
- 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.
- Downgrade or pin the previously working ty version while the fix lands.
- 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).
- 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
- Prefer released ty versions over local/nightly builds for production checks.
- ty developers: when adding a BDD operation, preserve existing constraints' SourceOrderId and register new ones through `calculate_source_orders`; never derive order from TDD traversal.
- Add an mdtest exercising any new rewrite with gradual (Unknown) bounds — those are the constraints silently lost when ordering is wrong.
- Run `cargo nextest run -p ty_python_semantic` after touching constraints.rs.
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
- clause vector should not be empty
- non-terminal BDD should have source_order
- every TDD constraint should have a source order
- `parsed_module` should have assigned a node index
- node should be non-terminal
AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20).
Data as JSON: /api/errors/f0d9a9180fb98edc.
Report an issue: GitHub.