astral-sh/ruff · error

non-terminal constraint set should have a source_order

Error message

non-terminal constraint set should have a source_order

What it means

The source-order counterpart of error 156 during a merge: if the incoming `OwnedConstraintSet` has a non-terminal root, it must also carry a `source_order` tree so the merged builder can map it. The expect fires when the incoming set has a non-terminal node but `source_order` is None.

Source

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

            if old_node.is_terminal() {
                return old_node;
            }
            if let Some(remapped) = cache.get(&old_node) {
                return *remapped;
            }

            let old_node_index = inner.retained_node_index(old_node);
            let old_interior = inner.nodes[old_node_index];
            let if_true = rebuild_node(storage, inner, constraints, cache, old_interior.if_true);
            let if_uncertain = rebuild_node(
                storage,
                inner,
                constraints,
                cache,
                old_interior.if_uncertain,
            );
            let if_false = rebuild_node(storage, inner, constraints, cache, old_interior.if_false);
            let old_constraint_index = inner.retained_constraint_index(old_interior.constraint);
            let (condition, _) = constraints[old_constraint_index];
            let remapped = condition.ite_uncertain(storage, if_true, if_uncertain, if_false);

            cache.insert(old_node, remapped);
            remapped
        }

        if other.node.is_terminal() {
            return (other.node, None);
        }
        let inner = other
            .inner
            .as_ref()
            .expect("storage-free owned constraint sets must have terminal roots");

        // Restore the saved order of referenced typevars before rebuilding constraints. A stored
        // `T <= U` can have `U` as its subject and `T` as its lower bound. Interning that subject
        // first would reverse the original typevar order, causing successive loads to alternate

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. Make the owned-set constructor enforce the pairing: non-terminal node implies both Some(inner) and Some(source_order).
  2. Check the producer path that built the incoming set (e.g. quantifier elimination) threads source_order like it threads the node.
  3. Add a regression test merging the output of every combine operation; run the ty_python_semantic suite.
  4. Report upstream with the fixture if unmodified ty panics.
Defensive patterns

Strategy: try-catch

Try / catch

let merged = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    builder.extend(db, env, other)
}))
.unwrap_or_else(|_| builder.clone_set());

Prevention

When it happens

Trigger: Merging an owned constraint set constructed non-terminal with source_order None - the same inconsistent-construction family as 152 and 156, surfacing at merge time instead of rebuild time.

Common situations: Changes to how owned sets are produced (quantification, caching into Salsa, partial moves out of a set); constraint mdtests with multiple merged sets after a solver refactor.

Related errors


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