Skip to main content

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