span/
ast_id.rs

1//! `AstIdMap` allows to create stable IDs for "large" syntax nodes like items
2//! and macro calls.
3//!
4//! Specifically, it enumerates all items in a file and uses position of a an
5//! item as an ID. That way, id's don't change unless the set of items itself
6//! changes.
7//!
8//! These IDs are tricky. If one of them invalidates, its interned ID invalidates,
9//! and this can cause *a lot* to be recomputed. For example, if you invalidate the ID
10//! of a struct, and that struct has an impl (any impl!) this will cause the `Self`
11//! type of the impl to invalidate, which will cause the all impls queries to be
12//! invalidated, which will cause every trait solve query in this crate *and* all
13//! transitive reverse dependencies to be invalidated, which is pretty much the worst
14//! thing that can happen incrementality wise.
15//!
16//! So we want these IDs to stay as stable as possible. For top-level items, we store
17//! their kind and name, which should be unique, but since they can still not be, we
18//! also store an index disambiguator. For nested items, we also store the ID of their
19//! parent. For macro calls, we store the macro name and an index. There aren't usually
20//! a lot of macro calls in item position, and invalidation in bodies is not much of
21//! a problem, so this should be enough.
22
23use std::{
24    any::type_name,
25    fmt,
26    hash::{BuildHasher, Hash, Hasher},
27    marker::PhantomData,
28};
29
30use la_arena::{Arena, Idx, RawIdx};
31use rustc_hash::{FxBuildHasher, FxHashMap};
32use syntax::{
33    AstNode, AstPtr, SyntaxKind, SyntaxNode, SyntaxNodePtr,
34    ast::{self, HasName},
35    match_ast,
36};
37
38// The first index is always the root node's AstId
39/// The root ast id always points to the encompassing file, using this in spans is discouraged as
40/// any range relative to it will be effectively absolute, ruining the entire point of anchored
41/// relative text ranges.
42pub const ROOT_ERASED_FILE_AST_ID: ErasedFileAstId =
43    ErasedFileAstId(pack_hash_index_and_kind(0, 0, ErasedFileAstIdKind::Root as u32));
44
45/// ErasedFileAstId used as the span for syntax node fixups. Any Span containing this file id is to be
46/// considered fake.
47pub const FIXUP_ERASED_FILE_AST_ID_MARKER: ErasedFileAstId =
48    ErasedFileAstId(pack_hash_index_and_kind(0, 0, ErasedFileAstIdKind::Fixup as u32));
49
50/// This is a type erased FileAstId.
51#[derive(Clone, Copy, PartialEq, Eq, Hash)]
52pub struct ErasedFileAstId(u32);
53
54impl fmt::Debug for ErasedFileAstId {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        let kind = self.kind();
57        macro_rules! kind {
58            ($($kind:ident),* $(,)?) => {
59                if false {
60                    // Ensure we covered all variants.
61                    match ErasedFileAstIdKind::Root {
62                        $( ErasedFileAstIdKind::$kind => {} )*
63                    }
64                    unreachable!()
65                }
66                $( else if kind == ErasedFileAstIdKind::$kind as u32 {
67                    stringify!($kind)
68                } )*
69                else {
70                    "Unknown"
71                }
72            };
73        }
74        let kind = kind!(
75            Root,
76            Enum,
77            Struct,
78            Union,
79            ExternCrate,
80            MacroDef,
81            MacroRules,
82            Module,
83            Static,
84            Trait,
85            TraitAlias,
86            Variant,
87            Const,
88            Fn,
89            MacroCall,
90            TypeAlias,
91            ExternBlock,
92            Use,
93            Impl,
94            BlockExpr,
95            AsmExpr,
96            Fixup,
97        );
98        if f.alternate() {
99            write!(f, "{kind}[{:04X}, {}]", self.hash_value(), self.index())
100        } else {
101            f.debug_struct("ErasedFileAstId")
102                .field("kind", &format_args!("{kind}"))
103                .field("index", &self.index())
104                .field("hash", &format_args!("{:04X}", self.hash_value()))
105                .finish()
106        }
107    }
108}
109
110#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
111#[repr(u8)]
112enum ErasedFileAstIdKind {
113    /// This needs to not change because it's depended upon by the proc macro server.
114    Fixup = 0,
115    // The following are associated with `ErasedHasNameFileAstId`.
116    Enum,
117    Struct,
118    Union,
119    ExternCrate,
120    MacroDef,
121    MacroRules,
122    Module,
123    Static,
124    Trait,
125    TraitAlias,
126    // Until here associated with `ErasedHasNameFileAstId`.
127    // The following are associated with `ErasedAssocItemFileAstId`.
128    Variant,
129    Const,
130    Fn,
131    MacroCall,
132    TypeAlias,
133    // Until here associated with `ErasedAssocItemFileAstId`.
134    // Extern blocks don't really have any identifying property unfortunately.
135    ExternBlock,
136    // FIXME: If we store the final `UseTree` instead of the top-level `Use`, we can store its name,
137    // and be way more granular for incrementality, at the expense of increased memory usage.
138    // Use IDs aren't used a lot. The main thing that stores them is the def map. So everything that
139    // uses the def map will be invalidated. That includes infers, and so is pretty bad, but our
140    // def map incrementality story is pretty bad anyway and needs to be improved (see
141    // https://rust-lang.zulipchat.com/#narrow/channel/185405-t-compiler.2Frust-analyzer/topic/.60infer.60.20queries.20and.20splitting.20.60DefMap.60).
142    // So I left this as-is for now, as the def map improvement should also mitigate this.
143    Use,
144    /// Associated with [`ImplFileAstId`].
145    Impl,
146    /// Associated with [`BlockExprFileAstId`].
147    BlockExpr,
148    // `global_asm!()` is an item, so we need to give it an `AstId`. So we give to all inline asm
149    // because incrementality is not a problem, they will always be the only item in the macro file,
150    // and memory usage also not because they're rare.
151    AsmExpr,
152    /// Keep this last.
153    Root,
154}
155
156// First hash, then index, then kind.
157const HASH_BITS: u32 = 16;
158const INDEX_BITS: u32 = 11;
159const KIND_BITS: u32 = 5;
160const _: () = assert!(ErasedFileAstIdKind::Fixup as u32 <= ((1 << KIND_BITS) - 1));
161const _: () = assert!(HASH_BITS + INDEX_BITS + KIND_BITS == u32::BITS);
162
163#[inline]
164const fn u16_hash(hash: u64) -> u16 {
165    // We do basically the same as `FxHasher`. We don't use rustc-hash and truncate because the
166    // higher bits have more entropy, but unlike rustc-hash we don't rotate because it rotates
167    // for hashmaps that just use the low bits, but we compare all bits.
168    const K: u16 = 0xecc5;
169    let (part1, part2, part3, part4) =
170        (hash as u16, (hash >> 16) as u16, (hash >> 32) as u16, (hash >> 48) as u16);
171    part1
172        .wrapping_add(part2)
173        .wrapping_mul(K)
174        .wrapping_add(part3)
175        .wrapping_mul(K)
176        .wrapping_add(part4)
177        .wrapping_mul(K)
178}
179
180#[inline]
181const fn pack_hash_index_and_kind(hash: u16, index: u32, kind: u32) -> u32 {
182    (hash as u32) | (index << HASH_BITS) | (kind << (HASH_BITS + INDEX_BITS))
183}
184
185impl ErasedFileAstId {
186    #[inline]
187    fn hash_value(self) -> u16 {
188        self.0 as u16
189    }
190
191    #[inline]
192    fn index(self) -> u32 {
193        (self.0 << KIND_BITS) >> (HASH_BITS + KIND_BITS)
194    }
195
196    #[inline]
197    fn kind(self) -> u32 {
198        self.0 >> (HASH_BITS + INDEX_BITS)
199    }
200
201    fn ast_id_for(
202        node: &SyntaxNode,
203        index_map: &mut ErasedAstIdNextIndexMap,
204        parent: Option<&ErasedFileAstId>,
205    ) -> Option<ErasedFileAstId> {
206        // Blocks are deliberately not here - we only want to allocate a block if it contains items.
207        has_name_ast_id(node, index_map)
208            .or_else(|| assoc_item_ast_id(node, index_map, parent))
209            .or_else(|| extern_block_ast_id(node, index_map))
210            .or_else(|| use_ast_id(node, index_map))
211            .or_else(|| impl_ast_id(node, index_map))
212            .or_else(|| asm_expr_ast_id(node, index_map))
213    }
214
215    fn should_alloc(node: &SyntaxNode) -> bool {
216        let kind = node.kind();
217        should_alloc_has_name(kind)
218            || should_alloc_assoc_item(kind)
219            || ast::ExternBlock::can_cast(kind)
220            || ast::Use::can_cast(kind)
221            || ast::Impl::can_cast(kind)
222            || ast::AsmExpr::can_cast(kind)
223    }
224
225    #[inline]
226    pub fn into_raw(self) -> u32 {
227        self.0
228    }
229
230    #[inline]
231    pub const fn from_raw(v: u32) -> Self {
232        Self(v)
233    }
234}
235
236pub trait AstIdNode: AstNode {}
237
238/// `AstId` points to an AST node in a specific file.
239pub struct FileAstId<N> {
240    raw: ErasedFileAstId,
241    _marker: PhantomData<fn() -> N>,
242}
243
244/// Traits are manually implemented because `derive` adds redundant bounds.
245impl<N> Clone for FileAstId<N> {
246    #[inline]
247    fn clone(&self) -> FileAstId<N> {
248        *self
249    }
250}
251impl<N> Copy for FileAstId<N> {}
252
253impl<N> PartialEq for FileAstId<N> {
254    fn eq(&self, other: &Self) -> bool {
255        self.raw == other.raw
256    }
257}
258impl<N> Eq for FileAstId<N> {}
259impl<N> Hash for FileAstId<N> {
260    fn hash<H: Hasher>(&self, hasher: &mut H) {
261        self.raw.hash(hasher);
262    }
263}
264
265impl<N> fmt::Debug for FileAstId<N> {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        write!(f, "FileAstId::<{}>({:?})", type_name::<N>(), self.raw)
268    }
269}
270
271impl<N> FileAstId<N> {
272    // Can't make this a From implementation because of coherence
273    #[inline]
274    pub fn upcast<M: AstIdNode>(self) -> FileAstId<M>
275    where
276        N: Into<M>,
277    {
278        FileAstId { raw: self.raw, _marker: PhantomData }
279    }
280
281    #[inline]
282    pub fn erase(self) -> ErasedFileAstId {
283        self.raw
284    }
285}
286
287#[derive(Hash)]
288struct ErasedHasNameFileAstId<'a> {
289    name: &'a str,
290}
291
292/// This holds the ast ID for variants too (they're a kind of assoc item).
293#[derive(Hash)]
294struct ErasedAssocItemFileAstId<'a> {
295    /// Subtle: items in `extern` blocks **do not** store the ID of the extern block here.
296    /// Instead this is left empty. The reason is that `ExternBlockFileAstId` is pretty unstable
297    /// (it contains only an index), and extern blocks don't introduce a new scope, so storing
298    /// the extern block ID will do more harm to incrementality than help.
299    parent: Option<ErasedFileAstId>,
300    properties: ErasedHasNameFileAstId<'a>,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Hash)]
304struct ImplFileAstId<'a> {
305    /// This can be `None` if the `Self` type is not a named type, or if it is inside a macro call.
306    self_ty_name: Option<&'a str>,
307    /// This can be `None` if this is an inherent impl, or if the trait name is inside a macro call.
308    trait_name: Option<&'a str>,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Hash)]
312struct BlockExprFileAstId {
313    parent: Option<ErasedFileAstId>,
314}
315
316impl AstIdNode for ast::ExternBlock {}
317
318fn extern_block_ast_id(
319    node: &SyntaxNode,
320    index_map: &mut ErasedAstIdNextIndexMap,
321) -> Option<ErasedFileAstId> {
322    if ast::ExternBlock::can_cast(node.kind()) {
323        Some(index_map.new_id(ErasedFileAstIdKind::ExternBlock, ()))
324    } else {
325        None
326    }
327}
328
329impl AstIdNode for ast::Use {}
330
331fn use_ast_id(
332    node: &SyntaxNode,
333    index_map: &mut ErasedAstIdNextIndexMap,
334) -> Option<ErasedFileAstId> {
335    if ast::Use::can_cast(node.kind()) {
336        Some(index_map.new_id(ErasedFileAstIdKind::Use, ()))
337    } else {
338        None
339    }
340}
341
342impl AstIdNode for ast::AsmExpr {}
343
344fn asm_expr_ast_id(
345    node: &SyntaxNode,
346    index_map: &mut ErasedAstIdNextIndexMap,
347) -> Option<ErasedFileAstId> {
348    if ast::AsmExpr::can_cast(node.kind()) {
349        Some(index_map.new_id(ErasedFileAstIdKind::AsmExpr, ()))
350    } else {
351        None
352    }
353}
354
355impl AstIdNode for ast::Impl {}
356
357fn impl_ast_id(
358    node: &SyntaxNode,
359    index_map: &mut ErasedAstIdNextIndexMap,
360) -> Option<ErasedFileAstId> {
361    if let Some(node) = ast::Impl::cast(node.clone()) {
362        let type_as_name = |ty: Option<ast::Type>| match ty? {
363            ast::Type::PathType(it) => Some(it.path()?.segment()?.name_ref()?),
364            _ => None,
365        };
366        let self_ty_name = type_as_name(node.self_ty());
367        let trait_name = type_as_name(node.trait_());
368        let data = ImplFileAstId {
369            self_ty_name: self_ty_name.as_ref().map(|it| it.text_non_mutable()),
370            trait_name: trait_name.as_ref().map(|it| it.text_non_mutable()),
371        };
372        Some(index_map.new_id(ErasedFileAstIdKind::Impl, data))
373    } else {
374        None
375    }
376}
377
378// Blocks aren't `AstIdNode`s deliberately, because unlike other nodes, not all blocks get their own
379// ast id, only if they have items. To account for that we have a different, fallible, API for blocks.
380// impl !AstIdNode for ast::BlockExpr {}
381
382fn block_expr_ast_id(
383    node: &SyntaxNode,
384    index_map: &mut ErasedAstIdNextIndexMap,
385    parent: Option<&ErasedFileAstId>,
386) -> Option<ErasedFileAstId> {
387    if ast::BlockExpr::can_cast(node.kind()) {
388        Some(
389            index_map.new_id(
390                ErasedFileAstIdKind::BlockExpr,
391                BlockExprFileAstId { parent: parent.copied() },
392            ),
393        )
394    } else {
395        None
396    }
397}
398
399#[derive(Default)]
400struct ErasedAstIdNextIndexMap(FxHashMap<(ErasedFileAstIdKind, u16), u32>);
401
402impl ErasedAstIdNextIndexMap {
403    #[inline]
404    fn new_id(&mut self, kind: ErasedFileAstIdKind, data: impl Hash) -> ErasedFileAstId {
405        let hash = FxBuildHasher.hash_one(&data);
406        let initial_hash = u16_hash(hash);
407        // Even though 2^INDEX_BITS=2048 items with the same hash seems like a lot,
408        // it could happen with macro calls or `use`s in macro-generated files. So we want
409        // to handle it gracefully. We just increment the hash.
410        let mut hash = initial_hash;
411        let index = loop {
412            match self.0.entry((kind, hash)) {
413                std::collections::hash_map::Entry::Occupied(mut entry) => {
414                    let i = entry.get_mut();
415                    if *i < ((1 << INDEX_BITS) - 1) {
416                        *i += 1;
417                        break *i;
418                    }
419                }
420                std::collections::hash_map::Entry::Vacant(entry) => {
421                    entry.insert(0);
422                    break 0;
423                }
424            }
425            hash = hash.wrapping_add(1);
426            if hash == initial_hash {
427                // That's 2^27=134,217,728 items!
428                panic!("you have way too many items in the same file!");
429            }
430        };
431        let kind = kind as u32;
432        ErasedFileAstId(pack_hash_index_and_kind(hash, index, kind))
433    }
434}
435
436macro_rules! register_enum_ast_id {
437    (impl $AstIdNode:ident for $($ident:ident),+ ) => {
438        $(
439            impl $AstIdNode for ast::$ident {}
440        )+
441    };
442}
443register_enum_ast_id! {
444    impl AstIdNode for
445    Item, AnyHasGenericParams, Adt, Macro,
446    AssocItem
447}
448
449macro_rules! register_has_name_ast_id {
450    (impl $AstIdNode:ident for $($ident:ident = $name_method:ident),+ ) => {
451        $(
452            impl $AstIdNode for ast::$ident {}
453        )+
454
455        fn has_name_ast_id(node: &SyntaxNode, index_map: &mut ErasedAstIdNextIndexMap) -> Option<ErasedFileAstId> {
456            match_ast! {
457                match node {
458                    $(
459                        ast::$ident(node) => {
460                            let name = node.$name_method();
461                            let name = name.as_ref().map_or("", |it| it.text_non_mutable());
462                            let result = ErasedHasNameFileAstId {
463                                name,
464                            };
465                            Some(index_map.new_id(ErasedFileAstIdKind::$ident, result))
466                        },
467                    )*
468                    _ => None,
469                }
470            }
471        }
472
473        fn should_alloc_has_name(kind: SyntaxKind) -> bool {
474            false $( || ast::$ident::can_cast(kind) )*
475        }
476    };
477}
478register_has_name_ast_id! {
479    impl AstIdNode for
480        Enum = name,
481        Struct = name,
482        Union = name,
483        ExternCrate = name_ref,
484        MacroDef = name,
485        MacroRules = name,
486        Module = name,
487        Static = name,
488        Trait = name
489}
490
491macro_rules! register_assoc_item_ast_id {
492    (impl $AstIdNode:ident for $($ident:ident = $name_callback:expr),+ ) => {
493        $(
494            impl $AstIdNode for ast::$ident {}
495        )+
496
497        fn assoc_item_ast_id(
498            node: &SyntaxNode,
499            index_map: &mut ErasedAstIdNextIndexMap,
500            parent: Option<&ErasedFileAstId>,
501        ) -> Option<ErasedFileAstId> {
502            match_ast! {
503                match node {
504                    $(
505                        ast::$ident(node) => {
506                            let name = $name_callback(node);
507                            let name = name.as_ref().map_or("", |it| it.text_non_mutable());
508                            let properties = ErasedHasNameFileAstId {
509                                name,
510                            };
511                            let result = ErasedAssocItemFileAstId {
512                                parent: parent.copied(),
513                                properties,
514                            };
515                            Some(index_map.new_id(ErasedFileAstIdKind::$ident, result))
516                        },
517                    )*
518                    _ => None,
519                }
520            }
521        }
522
523        fn should_alloc_assoc_item(kind: SyntaxKind) -> bool {
524            false $( || ast::$ident::can_cast(kind) )*
525        }
526    };
527}
528register_assoc_item_ast_id! {
529    impl AstIdNode for
530    Variant = |it: ast::Variant| it.name(),
531    Const = |it: ast::Const| it.name(),
532    Fn = |it: ast::Fn| it.name(),
533    MacroCall = |it: ast::MacroCall| it.path().and_then(|path| path.segment()?.name_ref()),
534    TypeAlias = |it: ast::TypeAlias| it.name()
535}
536
537/// Maps items' `SyntaxNode`s to `ErasedFileAstId`s and back.
538#[derive(Default)]
539pub struct AstIdMap {
540    /// An arena of the ptrs and their associated ID.
541    arena: Arena<(SyntaxNodePtr, ErasedFileAstId)>,
542    /// Map ptr to id.
543    ptr_map: hashbrown::HashTable<ArenaId>,
544    /// Map id to ptr.
545    id_map: hashbrown::HashTable<ArenaId>,
546}
547
548type ArenaId = Idx<(SyntaxNodePtr, ErasedFileAstId)>;
549
550impl fmt::Debug for AstIdMap {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        f.debug_struct("AstIdMap").field("arena", &self.arena).finish()
553    }
554}
555
556impl PartialEq for AstIdMap {
557    fn eq(&self, other: &Self) -> bool {
558        self.arena == other.arena
559    }
560}
561impl Eq for AstIdMap {}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564enum ContainsItems {
565    Yes,
566    No,
567}
568
569impl AstIdMap {
570    pub fn from_source(node: &SyntaxNode) -> AstIdMap {
571        assert!(node.parent().is_none());
572        let mut res = AstIdMap::default();
573        let mut index_map = ErasedAstIdNextIndexMap::default();
574
575        // Ensure we allocate the root.
576        res.arena.alloc((SyntaxNodePtr::new(node), ROOT_ERASED_FILE_AST_ID));
577
578        // By walking the tree in breadth-first order we make sure that parents
579        // get lower ids then children. That is, adding a new child does not
580        // change parent's id. This means that, say, adding a new function to a
581        // trait does not change ids of top-level items, which helps caching.
582
583        // This contains the stack of the `BlockExpr`s we are under. We do this
584        // so we only allocate `BlockExpr`s if they contain items.
585        // The general idea is: when we enter a block we push `(block, false)` here.
586        // Items inside the block are attributed to the block's container, not the block.
587        // For the first item we find inside a block, we make this `(block, true)`
588        // and create an ast id for the block. When exiting the block we pop it,
589        // whether or not we created an ast id for it.
590        // It may seem that with this setup we will generate an ID for blocks that
591        // have no items directly but have items inside other items inside them.
592        // This is true, but it doesn't matter, because such blocks can't exist.
593        // After all, the block will then contain the *outer* item, so we allocate
594        // an ID for it anyway.
595        let mut blocks = Vec::new();
596        let mut curr_layer = vec![(node.clone(), None)];
597        let mut next_layer = vec![];
598        while !curr_layer.is_empty() {
599            curr_layer.drain(..).for_each(|(node, parent_idx)| {
600                let mut preorder = node.preorder();
601                while let Some(event) = preorder.next() {
602                    match event {
603                        syntax::WalkEvent::Enter(node) => {
604                            if ast::BlockExpr::can_cast(node.kind()) {
605                                blocks.push((node, ContainsItems::No));
606                            } else if ErasedFileAstId::should_alloc(&node) {
607                                // Allocate blocks on-demand, only if they have items.
608                                // We don't associate items with blocks, only with items, since block IDs can be quite unstable.
609                                // FIXME: Is this the correct thing to do? Macro calls might actually be more incremental if
610                                // associated with blocks (not sure). Either way it's not a big deal.
611                                if let Some((
612                                    last_block_node,
613                                    already_allocated @ ContainsItems::No,
614                                )) = blocks.last_mut()
615                                {
616                                    let block_ast_id = block_expr_ast_id(
617                                        last_block_node,
618                                        &mut index_map,
619                                        parent_of(parent_idx, &res),
620                                    )
621                                    .expect("not a BlockExpr");
622                                    res.arena
623                                        .alloc((SyntaxNodePtr::new(last_block_node), block_ast_id));
624                                    *already_allocated = ContainsItems::Yes;
625                                }
626
627                                let parent = parent_of(parent_idx, &res);
628                                let ast_id =
629                                    ErasedFileAstId::ast_id_for(&node, &mut index_map, parent)
630                                        .expect("this node should have an ast id");
631                                let idx = res.arena.alloc((SyntaxNodePtr::new(&node), ast_id));
632
633                                next_layer.extend(node.children().map(|child| (child, Some(idx))));
634                                preorder.skip_subtree();
635                            }
636                        }
637                        syntax::WalkEvent::Leave(node) => {
638                            if ast::BlockExpr::can_cast(node.kind()) {
639                                assert_eq!(
640                                    blocks.pop().map(|it| it.0),
641                                    Some(node),
642                                    "left a BlockExpr we never entered"
643                                );
644                            }
645                        }
646                    }
647                }
648            });
649            std::mem::swap(&mut curr_layer, &mut next_layer);
650            assert!(blocks.is_empty(), "didn't leave all BlockExprs");
651        }
652
653        res.ptr_map = hashbrown::HashTable::with_capacity(res.arena.len());
654        res.id_map = hashbrown::HashTable::with_capacity(res.arena.len());
655        for (idx, (ptr, ast_id)) in res.arena.iter() {
656            let ptr_hash = hash_ptr(ptr);
657            let ast_id_hash = hash_ast_id(ast_id);
658            match res.ptr_map.entry(
659                ptr_hash,
660                |idx2| *idx2 == idx,
661                |&idx| hash_ptr(&res.arena[idx].0),
662            ) {
663                hashbrown::hash_table::Entry::Occupied(_) => unreachable!(),
664                hashbrown::hash_table::Entry::Vacant(entry) => {
665                    entry.insert(idx);
666                }
667            }
668            match res.id_map.entry(
669                ast_id_hash,
670                |idx2| *idx2 == idx,
671                |&idx| hash_ast_id(&res.arena[idx].1),
672            ) {
673                hashbrown::hash_table::Entry::Occupied(_) => unreachable!(),
674                hashbrown::hash_table::Entry::Vacant(entry) => {
675                    entry.insert(idx);
676                }
677            }
678        }
679        res.arena.shrink_to_fit();
680        return res;
681
682        fn parent_of(parent_idx: Option<ArenaId>, res: &AstIdMap) -> Option<&ErasedFileAstId> {
683            let mut parent = parent_idx.map(|parent_idx| &res.arena[parent_idx].1);
684            if parent.is_some_and(|parent| parent.kind() == ErasedFileAstIdKind::ExternBlock as u32)
685            {
686                // See the comment on `ErasedAssocItemFileAstId` for why is this.
687                // FIXME: Technically there could be an extern block inside another item, e.g.:
688                // ```
689                // fn foo() {
690                //     extern "C" {
691                //         fn bar();
692                //     }
693                // }
694                // ```
695                // Here we want to make `foo()` the parent of `bar()`, but we make it `None`.
696                // Shouldn't be a big deal though.
697                parent = None;
698            }
699            parent
700        }
701    }
702
703    /// The [`AstId`] of the root node
704    pub fn root(&self) -> SyntaxNodePtr {
705        self.arena[Idx::from_raw(RawIdx::from_u32(0))].0
706    }
707
708    pub fn ast_id<N: AstIdNode>(&self, item: &N) -> FileAstId<N> {
709        self.ast_id_for_ptr(AstPtr::new(item))
710    }
711
712    /// Blocks may not be allocated (if they have no items), so they have a different API.
713    pub fn ast_id_for_block(&self, block: &ast::BlockExpr) -> Option<FileAstId<ast::BlockExpr>> {
714        self.ast_id_for_ptr_for_block(AstPtr::new(block))
715    }
716
717    pub fn ast_id_for_ptr<N: AstIdNode>(&self, ptr: AstPtr<N>) -> FileAstId<N> {
718        let ptr = ptr.syntax_node_ptr();
719        FileAstId { raw: self.erased_ast_id(ptr), _marker: PhantomData }
720    }
721
722    /// Blocks may not be allocated (if they have no items), so they have a different API.
723    pub fn ast_id_for_ptr_for_block(
724        &self,
725        ptr: AstPtr<ast::BlockExpr>,
726    ) -> Option<FileAstId<ast::BlockExpr>> {
727        let ptr = ptr.syntax_node_ptr();
728        self.try_erased_ast_id(ptr).map(|raw| FileAstId { raw, _marker: PhantomData })
729    }
730
731    fn erased_ast_id(&self, ptr: SyntaxNodePtr) -> ErasedFileAstId {
732        self.try_erased_ast_id(ptr).unwrap_or_else(|| {
733            panic!(
734                "Can't find SyntaxNodePtr {:?} in AstIdMap:\n{:?}",
735                ptr,
736                self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
737            )
738        })
739    }
740
741    fn try_erased_ast_id(&self, ptr: SyntaxNodePtr) -> Option<ErasedFileAstId> {
742        let hash = hash_ptr(&ptr);
743        let idx = *self.ptr_map.find(hash, |&idx| self.arena[idx].0 == ptr)?;
744        Some(self.arena[idx].1)
745    }
746
747    // Don't bound on `AstIdNode` here, because `BlockExpr`s are also valid here (`ast::BlockExpr`
748    // doesn't always have a matching `FileAstId`, but a `FileAstId<ast::BlockExpr>` always has
749    // a matching node).
750    pub fn get<N: AstNode>(&self, id: FileAstId<N>) -> AstPtr<N> {
751        let ptr = self.get_erased(id.raw);
752        AstPtr::try_from_raw(ptr)
753            .unwrap_or_else(|| panic!("AstIdMap node mismatch with node `{ptr:?}`"))
754    }
755
756    pub fn get_erased(&self, id: ErasedFileAstId) -> SyntaxNodePtr {
757        let hash = hash_ast_id(&id);
758        match self.id_map.find(hash, |&idx| self.arena[idx].1 == id) {
759            Some(&idx) => self.arena[idx].0,
760            None => panic!(
761                "Can't find ast id {:?} in AstIdMap:\n{:?}",
762                id,
763                self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
764            ),
765        }
766    }
767}
768
769#[inline]
770fn hash_ptr(ptr: &SyntaxNodePtr) -> u64 {
771    FxBuildHasher.hash_one(ptr)
772}
773
774#[inline]
775fn hash_ast_id(ptr: &ErasedFileAstId) -> u64 {
776    FxBuildHasher.hash_one(ptr)
777}
778
779#[cfg(test)]
780mod tests {
781    use syntax::{AstNode, Edition, SourceFile, SyntaxKind, SyntaxNodePtr, WalkEvent, ast};
782
783    use crate::AstIdMap;
784
785    #[test]
786    fn check_all_nodes() {
787        let syntax = SourceFile::parse(
788            r#"
789extern crate foo;
790fn foo() {
791    union U {}
792}
793struct S;
794macro_rules! m {}
795macro m2() {}
796trait Trait {}
797impl Trait for S {}
798impl S {}
799impl m!() {}
800impl m2!() for m!() {}
801type T = i32;
802enum E {
803    V1(),
804    V2 {},
805    V3,
806}
807struct S; // duplicate
808extern "C" {
809    static S: i32;
810}
811static mut S: i32 = 0;
812const FOO: i32 = 0;
813        "#,
814            Edition::CURRENT,
815        )
816        .syntax_node();
817        let ast_id_map = AstIdMap::from_source(&syntax);
818        for node in syntax.preorder() {
819            let WalkEvent::Enter(node) = node else { continue };
820            if !matches!(
821                node.kind(),
822                SyntaxKind::EXTERN_CRATE
823                    | SyntaxKind::FN
824                    | SyntaxKind::UNION
825                    | SyntaxKind::STRUCT
826                    | SyntaxKind::MACRO_RULES
827                    | SyntaxKind::MACRO_DEF
828                    | SyntaxKind::MACRO_CALL
829                    | SyntaxKind::TRAIT
830                    | SyntaxKind::IMPL
831                    | SyntaxKind::TYPE_ALIAS
832                    | SyntaxKind::ENUM
833                    | SyntaxKind::VARIANT
834                    | SyntaxKind::EXTERN_BLOCK
835                    | SyntaxKind::STATIC
836                    | SyntaxKind::CONST
837            ) {
838                continue;
839            }
840            let ptr = SyntaxNodePtr::new(&node);
841            let ast_id = ast_id_map.erased_ast_id(ptr);
842            let turn_back = ast_id_map.get_erased(ast_id);
843            assert_eq!(ptr, turn_back);
844        }
845    }
846
847    #[test]
848    fn different_names_get_different_hashes() {
849        let syntax = SourceFile::parse(
850            r#"
851fn foo() {}
852fn bar() {}
853        "#,
854            Edition::CURRENT,
855        )
856        .syntax_node();
857        let ast_id_map = AstIdMap::from_source(&syntax);
858        let fns = syntax.descendants().filter_map(ast::Fn::cast).collect::<Vec<_>>();
859        let [foo_fn, bar_fn] = fns.as_slice() else {
860            panic!("not exactly 2 functions");
861        };
862        let foo_fn_id = ast_id_map.ast_id(foo_fn);
863        let bar_fn_id = ast_id_map.ast_id(bar_fn);
864        assert_ne!(foo_fn_id.raw.hash_value(), bar_fn_id.raw.hash_value(), "hashes are equal");
865    }
866
867    #[test]
868    fn different_parents_get_different_hashes() {
869        let syntax = SourceFile::parse(
870            r#"
871fn foo() {
872    m!();
873}
874fn bar() {
875    m!();
876}
877        "#,
878            Edition::CURRENT,
879        )
880        .syntax_node();
881        let ast_id_map = AstIdMap::from_source(&syntax);
882        let macro_calls = syntax.descendants().filter_map(ast::MacroCall::cast).collect::<Vec<_>>();
883        let [macro_call_foo, macro_call_bar] = macro_calls.as_slice() else {
884            panic!("not exactly 2 macro calls");
885        };
886        let macro_call_foo_id = ast_id_map.ast_id(macro_call_foo);
887        let macro_call_bar_id = ast_id_map.ast_id(macro_call_bar);
888        assert_ne!(
889            macro_call_foo_id.raw.hash_value(),
890            macro_call_bar_id.raw.hash_value(),
891            "hashes are equal"
892        );
893    }
894
895    #[test]
896    fn blocks_with_no_items_have_no_id() {
897        let syntax = SourceFile::parse(
898            r#"
899fn foo() {
900    let foo = 1;
901    bar(foo);
902}
903        "#,
904            Edition::CURRENT,
905        )
906        .syntax_node();
907        let ast_id_map = AstIdMap::from_source(&syntax);
908        let block = syntax.descendants().find_map(ast::BlockExpr::cast).expect("no block");
909        assert!(ast_id_map.ast_id_for_block(&block).is_none());
910    }
911}