astral-sh/ruff · critical

clause vector should not be empty

Error message

clause vector should not be empty

What it means

Internal assertion in `SatisfiedClause::pop` (crates/ty_python_semantic/src/types/constraints.rs:7038). `SatisfiedClause` is one conjunction in the DNF representation that ty builds while enumerating all variable assignments satisfying a BDD; the `Searcher` in `satisfied_clauses` (constraints.rs:3274-3292) keeps strictly paired `push(constraint)`/`visit`/`pop()` steps for each of an interior node's three branches (if_true, if_uncertain, if_false) during depth-first traversal and backtracking. The `expect` asserts that push/pop bookkeeping stays symmetric; a panic means the traversal popped a constraint it never pushed, an internal solver bug unrelated to the Python code being checked.

Source

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

        check_solutions_for_constraint_orderings(
            db,
            &[t, u],
            &atoms,
            |storage| {
                let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(db, &env, storage));
                t_int
                    .or(storage, t_str)
                    .negate(storage)
                    .or(storage, bytes_u)
            },
            // A satisfied alternative must not infer `T` from unrelated positive decisions
            // made earlier in a TDD path.
            ["never=false always=false merged=[U=bytes] paths=[; U=bytes]"],
        );
    }

    #[test]
    fn constraint_ordering_preserves_independent_concrete_solutions() {
        let db = setup_db();
        let db = &db;
        let env = db.program_environment();
        let t = create_typevar(db, "T");
        let u = create_typevar(db, "U");
        let int = KnownClass::Int.to_instance(db, &env);
        let str = KnownClass::Str.to_instance(db, &env);
        let atoms = [
            PermutedConstraint(
                t,
                ConstraintBound::missing_lower(),
                ConstraintBound::Evidence(int),
            ),
            PermutedConstraint(
                t,
                ConstraintBound::missing_lower(),
                ConstraintBound::Evidence(str),
            ),

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. If hit while developing ty: diff recent changes to `satisfied_clauses`/`SatisfiedClause` in crates/ty_python_semantic/src/types/constraints.rs and restore the push/pop pairing on every branch (including early returns and `continue`s).
  2. Run the solver unit tests: `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic` and add a test covering the edited branch shape.
  3. As an end user: report with `RUST_BACKTRACE=1 ty check <file>` output, the file, and `ty --version`; downgrade to the last working build meanwhile.
  4. Consider replacing the `Vec` backtracking with a persistent/cons-list passed by value so a desync cannot occur structurally.
Defensive patterns

Strategy: try-catch

Try / catch

// Embedders: per-file panic isolation keeps one ICE from killing a batch run
use std::panic::{catch_unwind, AssertUnwindSafe};

if catch_unwind(AssertUnwindSafe(|| run_check(db, file))).is_err() {
    mark_file_errored(file); // continue with remaining files
}

Prevention

When it happens

Trigger: Only reachable on code paths that enumerate DNF clauses of a BDD via `satisfied_clauses` (also used by the BDD `display` output, where an empty clause renders as "always"). It fires if someone edits the `Searcher` traversal so a branch pushes conditionally but pops unconditionally — or skips a push while keeping the pop — desynchronizing `current_clause`.

Common situations: Practically unreachable with the current strictly-paired traversal; it exists as a defensive invariant. It would surface during ty development after modifying the DNF enumeration or after a BDD rewrite produced a node shape the Searcher did not anticipate. Users on released ty builds essentially never see it.

Related errors


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