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

  1. Check that the children adjacency list is a proper tree: no shared or cyclic child indices
  2. Ensure `append_postorder` runs once per plan node and `ordered` is not built twice
  3. Fix any caller that mutates `planned` concurrently with traversal
  4. 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

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


AI-assisted analysis of rust-lang/rust-analyzer@e8f7e90aa3 (2026-09-03). Data as JSON: /api/errors/6391c2c41f553140. Report an issue: GitHub.