{"record":{"id":"f0d9a9180fb98edc","repo":"astral-sh/ruff","slug":"every-bdd-constraint-should-have-a-source-order-en","errorCode":null,"errorMessage":"every BDD constraint should have a source-order entry","messagePattern":"every BDD constraint should have a source-order entry","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/ty_python_semantic/src/types/constraints.rs","lineNumber":4713,"sourceCode":"        let self_ordering = self_interior.constraint.ordering();\n        let other_interior = storage.interior_node_data(other.node());\n        let other_ordering = other_interior.constraint.ordering();\n        let result = match self_ordering.cmp(&other_ordering) {\n            Ordering::Equal => {\n                let if_true = self_interior.if_true.or(storage, other_interior.if_true);\n                let if_uncertain = self_interior\n                    .if_uncertain\n                    .or(storage, other_interior.if_uncertain);\n                let if_false = self_interior.if_false.or(storage, other_interior.if_false);\n                NodeId::with_uncertain(\n                    storage,\n                    self_interior.constraint,\n                    if_true,\n                    if_uncertain,\n                    if_false,\n                )\n            }\n            // This is from Frisch's original description of TDDs. If self < other, we check self\n            // first. Instead of distributing other into the if_true and if_false branches, we\n            // \"park\" it in the if_uncertain branch. That causes us to only evaluate other \"lazily\"\n            // when needed.\n            Ordering::Less => {\n                let if_uncertain = self_interior.if_uncertain.or(storage, other.node());\n                NodeId::with_uncertain(\n                    storage,\n                    self_interior.constraint,\n                    self_interior.if_true,\n                    if_uncertain,\n                    self_interior.if_false,\n                )\n            }\n            // Ditto above but for the other variable ordering\n            Ordering::Greater => {\n                let if_uncertain = self.node().or(storage, other_interior.if_uncertain);\n                NodeId::with_uncertain(\n                    storage,","sourceCodeStart":4695,"sourceCodeEnd":4731,"githubUrl":"https://github.com/astral-sh/ruff/blob/15f3fe6b15a5f00172f34b0f542f8ea277f5a586/crates/ty_python_semantic/src/types/constraints.rs#L4695-L4731","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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`."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Embedders: same per-file panic isolation as other ty ICEs\nuse std::panic::{catch_unwind, AssertUnwindSafe};\n\nlet checked = catch_unwind(AssertUnwindSafe(|| analyze(db, file)));\nif checked.is_err() { log::error!(\"ty ICE in path_assignments for {file}\"); }","preventionTips":["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."],"tags":["ty","type-inference","constraint-solver","bdd","source-order","panic","ice","rust"],"backgroundTag":"compiler-internal-error","analyzedSha":"15f3fe6b15a5f00172f34b0f542f8ea277f5a586","analyzedAt":"2026-08-20T16:33:49.445Z","contentChangedAt":"2026-08-20T16:33:49.445Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}