rust-lang/rust-analyzer · error

AstIdMap node mismatch with node

Error message

AstIdMap node mismatch with node `{ptr:?}`

What it means

`AstIdMap::get` converts a stored FileAstId back into a typed AstPtr by first resolving the raw id to a SyntaxNodePtr and then re-typing it via `AstPtr::try_from_raw`. If the resolved pointer cannot be represented as the requested node type `N`, the encoded id/kind do not agree with the stored node — an internal encoding invariant violation — so it panics. This indicates the packed hash/index/kind encoding pointed at an entry whose kind does not match the requested type.

Solutions

  1. Verify the FileAstId was produced by the same AstIdMap instance you are querying
  2. Check pack/unpack of hash, index, and kind bits after changing the encoding
  3. Add/refresh the `check_all_nodes` consistency test to catch mismatches early
Defensive patterns

Strategy: validation

Validate before calling

// Before get::<N>, confirm the kind matches the requested type:
fn kind_matches<N: AstNode>(map: &AstIdMap, id: FileAstId<N>) -> bool {
    map.raw_kind(id.raw) == N::kind()
}

Try / catch

let ptr = std::panic::catch_unwind(AssertUnwindSafe(|| map.get::<ast::Item>(id))).ok()?;

Prevention

When it happens

Trigger: Calling `map.get::<N>(id)` with a FileAstId<N> whose stored kind bits don't match N's kind — typically caused by a bug in pack_hash_index_and_kind/unpacking, an id from a different AstIdMap being looked up in the wrong map, or hash collisions corrupting the id_map lookup.

Common situations: Developers changing the ErasedFileAstId bit layout or hashing; cross-file/cross-version id reuse in salsa-cached queries; fuzzing inputs that expose kind-mismatch corruption.

Related errors


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

Appendix: source

Thrown at crates/span/src/ast_id.rs:845

                ptr,
                self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
            )
        })
    }

    fn try_erased_ast_id(&self, ptr: SyntaxNodePtr) -> Option<ErasedFileAstId> {
        let hash = hash_ptr(&ptr);
        let idx = *self.ptr_map.find(hash, |&idx| self.arena[idx].0 == ptr)?;
        Some(self.arena[idx].1)
    }

    // Don't bound on `AstIdNode` here, because `BlockExpr`s are also valid here (`ast::BlockExpr`
    // doesn't always have a matching `FileAstId`, but a `FileAstId<ast::BlockExpr>` always has
    // a matching node).
    pub fn get<N: AstNode>(&self, id: FileAstId<N>) -> AstPtr<N> {
        let ptr = self.get_erased(id.raw);
        AstPtr::try_from_raw(ptr)
            .unwrap_or_else(|| panic!("AstIdMap node mismatch with node `{ptr:?}`"))
    }

    pub fn get_erased(&self, id: ErasedFileAstId) -> SyntaxNodePtr {
        let hash = hash_ast_id(&id);
        match self.id_map.find(hash, |&idx| self.arena[idx].1 == id) {
            Some(&idx) => self.arena[idx].0,
            None => panic!(
                "Can't find ast id {:?} in AstIdMap:\n{:?}",
                id,
                self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
            ),
        }
    }
}

#[cfg(not(no_salsa_async_drops))]
impl Drop for AstIdMap {
    fn drop(&mut self) {

View on GitHub (pinned to e8f7e90aa3)