astral-sh/ruff · error
storage-free owned constraint sets must have terminal roots
Error message
storage-free owned constraint sets must have terminal roots
What it means
When merging another `OwnedConstraintSet` into a builder, the incoming set's root is checked first: a terminal root needs no data, but a non-terminal root must carry its own inner storage (`other.inner`). The expect fires when an owned set was constructed with a non-terminal node but no inner storage - an inconsistent construction that the merge refuses to handle silently.
Source
Thrown at crates/ty_python_semantic/src/types/constraints.rs:1687
if index < split {
let compacted_index = compacted.retained_node_index(node);
return Some(compacted.node_supports[compacted_index]);
}
return Some(self.node_supports[NodeId::from_usize(index - split)]);
}
Some(self.node_supports[node])
}
fn node_support(&self, node: NodeId) -> Option<&Support> {
self.node_support_id(node)
.map(|support| self.support_data(support))
}
/// Loads an [`OwnedConstraintSet`] into this storage.
fn load(
&mut self,
db: &'db dyn Db,
env: &ProgramEnvironment<'db>,
other: &OwnedConstraintSet<'db>,
) -> (NodeId, Option<SourceOrderId>) {
fn rebuild_node<'db>(
storage: &mut ConstraintSetStorage<'db>,
inner: &OwnedConstraintSetInner<'db>,
constraints: &[(NodeId, Option<SourceOrderId>)],
cache: &mut FxHashMap<NodeId, NodeId>,
old_node: NodeId,
) -> NodeId {
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];View on GitHub (pinned to 15f3fe6b15)
Solutions
- Audit all `OwnedConstraintSet` constructors: non-terminal root requires Some(inner); terminal root requires None.
- Add a debug assertion in the constructor so misuse fails at creation, with context, instead of at merge time.
- Write a unit test that merges sets produced by every public combine operation.
- Report upstream with the reproducer if stock 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()); // skip the poisoned merge Prevention
- Contributors: assert the node/inner pairing in the OwnedConstraintSet constructor so misuse fails early.
- Do not cache partially-moved owned sets; take or clone them wholesale.
- Report with the sequence of operations that produced the incoming set.
When it happens
Trigger: Merging an `OwnedConstraintSet` whose `node` is non-terminal while `inner` is None - typically a constructor path or optimization that strips inner storage without also collapsing the root to a terminal node.
Common situations: Refactoring `OwnedConstraintSet` construction (caching, serialization, quantifier results); tests that hand-build sets; usually a compile-time-adjacent bug caught by constraint mdtests, not user Python code.
Related errors
- non-terminal constraint set should have a source_order
- non-terminal BDD should have source_order
- node should be non-terminal
- Expected `NamedTuple` definition r.h.s. to be a call express
- typevar should be interned before ordering
AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20).
Data as JSON: /api/errors/25d4f6da067cee9c.
Report an issue: GitHub.