rust-lang/rust-analyzer · error
reachable plan nodes are not discarded
Error message
reachable plan nodes are not discarded
What it means
During `append_postorder`, the planner pops each planned change out of the `planned` scratch slot via `planned[index].take()` while emitting the postorder. The `expect` asserts every reachable plan node is visited exactly once and still present; if a slot is already `None`, a node was consumed twice or discarded, which is an internal algorithm bug.
Source
Thrown at crates/syntax/src/syntax_editor/edit_algo.rs:199
right
.target_range()
.start()
.cmp(&left.target_range().start())
.then_with(|| node_depth(right.target_parent()).cmp(&node_depth(left.target_parent())))
.then(right.change_kind().cmp(&left.change_kind()))
}
/// Appends a dependency subtree in post order fashion.
fn append_postorder(
index: usize,
children: &[Vec<usize>],
planned: &mut [Option<PlannedChange>],
ordered: &mut Vec<PlannedChange>,
) {
for &child in &children[index] {
Self::append_postorder(child, children, planned, ordered);
}
ordered.push(planned[index].take().expect("reachable plan nodes are not discarded"));
}
/// Checks that replacement at the same tree depth do not overlap
///
/// `changes` is sorted by range start, so overlap is a single comparison against the
/// last range at that key, and `insert` can throw away the range it evicts.
fn replacements_are_disjoint(
changes: &[Change],
mut node_depth: impl FnMut(SyntaxNode) -> usize,
) -> bool {
let mut previous = FxHashMap::<(SyntaxNode, usize), TextRange>::default();
for change in changes {
if !matches!(change.change_kind(), ChangeKind::Replace | ChangeKind::ReplaceRange) {
continue;
}
let parent = change.target_parent();
let key = (parent.tree_top(), node_depth(parent));View on GitHub (pinned to e8f7e90aa3)
Solutions
- Check that the children adjacency list is a proper tree: no shared or cyclic child indices
- Ensure `append_postorder` runs once per plan node and `ordered` is not built twice
- Fix any caller that mutates `planned` concurrently with traversal
- If editing this code, guard with `stdx::never!` and skip already-taken slots instead of panicking
Example fix
// before
ordered.push(planned[index].take().expect("reachable plan nodes are not discarded"));
// after
if let Some(change) = planned[index].take() {
ordered.push(change);
} else {
stdx::never!("plan node {} already consumed", index);
} Defensive patterns
Strategy: validation
Validate before calling
fn assert_plan_is_tree(children: &[Vec<usize>]) -> bool {
let mut seen = vec![false; children.len()];
fn dfs(i: usize, children: &[Vec<usize>], seen: &mut [bool], on_stack: &mut [bool]) -> bool {
if on_stack[i] { return false; }
if seen[i] { return false; }
seen[i] = true; on_stack[i] = true;
let ok = children[i].iter().all(|&c| dfs(c, children, seen, on_stack));
on_stack[i] = false;
ok
}
let mut on_stack = vec![false; children.len()];
dfs(0, children, &mut seen, &mut on_stack)
} Prevention
- Keep the plan's children structure a strict tree: each node has exactly one parent
- Never reuse a PlannedChange slot after take()
- If hacking on edit_algo, add debug assertions that every index is visited exactly once
When it happens
Trigger: Hitting the planner with a children/graph structure where the same plan node index is reachable from two parents (a cycle or shared/diamond child reference), or any internal state where `planned` slots were taken before all parents visited them. Not triggerable through the public SyntaxEditor API by user input alone.
Common situations: Developers modifying the edit_algo planning code, introducing cycles or duplicate child indices in the `children` adjacency structure, or re-running `append_postorder` on the same plan twice in custom edits.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- an edit target must still be present
- syntax annotation id overflow
- equivalent ancestor node should be present in target tree
- the nearest mapped ancestor must map its descendants
- We explicitly do not provide canonicalization API, as that i
AI-assisted analysis of rust-lang/rust-analyzer@e8f7e90aa3 (2026-09-03).
Data as JSON: /api/errors/6391c2c41f553140.
Report an issue: GitHub.