{"record":{"id":"6391c2c41f553140","repo":"rust-lang/rust-analyzer","slug":"reachable-plan-nodes-are-not-discarded","errorCode":null,"errorMessage":"reachable plan nodes are not discarded","messagePattern":"reachable plan nodes are not discarded","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/syntax/src/syntax_editor/edit_algo.rs","lineNumber":199,"sourceCode":"        right\n            .target_range()\n            .start()\n            .cmp(&left.target_range().start())\n            .then_with(|| node_depth(right.target_parent()).cmp(&node_depth(left.target_parent())))\n            .then(right.change_kind().cmp(&left.change_kind()))\n    }\n\n    /// Appends a dependency subtree in post order fashion.\n    fn append_postorder(\n        index: usize,\n        children: &[Vec<usize>],\n        planned: &mut [Option<PlannedChange>],\n        ordered: &mut Vec<PlannedChange>,\n    ) {\n        for &child in &children[index] {\n            Self::append_postorder(child, children, planned, ordered);\n        }\n        ordered.push(planned[index].take().expect(\"reachable plan nodes are not discarded\"));\n    }\n\n    /// Checks that replacement at the same tree depth do not overlap\n    ///\n    /// `changes` is sorted by range start, so overlap is a single comparison against the\n    /// last range at that key, and `insert` can throw away the range it evicts.\n    fn replacements_are_disjoint(\n        changes: &[Change],\n        mut node_depth: impl FnMut(SyntaxNode) -> usize,\n    ) -> bool {\n        let mut previous = FxHashMap::<(SyntaxNode, usize), TextRange>::default();\n        for change in changes {\n            if !matches!(change.change_kind(), ChangeKind::Replace | ChangeKind::ReplaceRange) {\n                continue;\n            }\n\n            let parent = change.target_parent();\n            let key = (parent.tree_top(), node_depth(parent));","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/rust-lang/rust-analyzer/blob/e8f7e90aa3e7b26aa9a000200f606c1078da99ec/crates/syntax/src/syntax_editor/edit_algo.rs#L181-L217","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nordered.push(planned[index].take().expect(\"reachable plan nodes are not discarded\"));\n// after\nif let Some(change) = planned[index].take() {\n    ordered.push(change);\n} else {\n    stdx::never!(\"plan node {} already consumed\", index);\n}","handlingStrategy":"validation","validationCode":"fn assert_plan_is_tree(children: &[Vec<usize>]) -> bool {\n    let mut seen = vec![false; children.len()];\n    fn dfs(i: usize, children: &[Vec<usize>], seen: &mut [bool], on_stack: &mut [bool]) -> bool {\n        if on_stack[i] { return false; }\n        if seen[i] { return false; }\n        seen[i] = true; on_stack[i] = true;\n        let ok = children[i].iter().all(|&c| dfs(c, children, seen, on_stack));\n        on_stack[i] = false;\n        ok\n    }\n    let mut on_stack = vec![false; children.len()];\n    dfs(0, children, &mut seen, &mut on_stack)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","internal-invariant","panic","syntax-editor"],"backgroundTag":"internal-invariant-violation","analyzedSha":"e8f7e90aa3e7b26aa9a000200f606c1078da99ec","analyzedAt":"2026-09-03T21:08:06.959Z","contentChangedAt":"2026-09-03T21:08:06.959Z","schemaVersion":2},"datasetVersion":"2026-09-11T07:07:21.782Z"}