llmcc_core/
context.rs

1use parking_lot::RwLock;
2use rayon::prelude::*;
3use smallvec::SmallVec;
4use std::cmp::Ordering as CmpOrdering;
5use std::fs;
6use std::io::Write;
7use std::ops::Deref;
8use std::time::Instant;
9use tree_sitter::Node;
10use uuid::Uuid;
11
12use crate::block::{BasicBlock, BlockArena, BlockId, reset_block_id_counter};
13use crate::block_rel::{BlockIndexMaps, BlockRelationMap};
14use crate::file::File;
15use crate::meta::{UnitMeta, UnitMetaBuilder};
16
17/// Controls how files are ordered after parallel reading.
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19pub enum FileOrder {
20    /// Preserve the original input order (deterministic, good for tests).
21    #[default]
22    Original,
23    /// Sort by file size descending (better parallel load balancing for large projects).
24    BySizeDescending,
25}
26use crate::interner::{InternPool, InternedStr};
27use crate::ir::{Arena, HirBase, HirId, HirIdent, HirKind, HirNode};
28use crate::ir_builder::reset_hir_id_counter;
29use crate::lang_def::{LanguageTrait, ParseTree};
30use crate::scope::Scope;
31use crate::symbol::{ScopeId, SymId, Symbol, reset_scope_id_counter, reset_symbol_id_counter};
32
33/// Thread-safe wrapper for raw Symbol pointer.
34/// SAFETY: The Symbol is allocated in a bump arena that outlives all usage,
35/// and Symbols are immutable after creation (interior mutability via atomics).
36#[derive(Clone, Copy)]
37pub struct SymbolPtr(*const Symbol);
38
39// SAFETY: Symbol is allocated in a thread-local bump arena, but the backing memory
40// is stable for the lifetime of the arena. Symbol fields use AtomicCell/AtomicUsize
41// for interior mutability, making them safe to access from multiple threads.
42unsafe impl Send for SymbolPtr {}
43unsafe impl Sync for SymbolPtr {}
44
45impl SymbolPtr {
46    pub fn new(ptr: *const Symbol) -> Self {
47        Self(ptr)
48    }
49
50    pub fn as_ptr(&self) -> *const Symbol {
51        self.0
52    }
53}
54
55/// Thread-safe wrapper for raw Scope pointer.
56#[derive(Clone, Copy)]
57pub struct ScopePtr(*const Scope<'static>);
58
59// SAFETY: Scope is allocated in a thread-local bump arena, but the backing memory
60// is stable for the lifetime of the arena. Scope fields use RwLock/DashMap
61// for interior mutability, making them safe to access from multiple threads.
62unsafe impl Send for ScopePtr {}
63unsafe impl Sync for ScopePtr {}
64
65impl ScopePtr {
66    pub fn new<'a>(ptr: *const Scope<'a>) -> Self {
67        // Erase lifetime - the pointer is valid for the arena's lifetime
68        // SAFETY: We're casting to erase the lifetime, which is intentional
69        Self(ptr.cast())
70    }
71
72    /// Get the raw pointer with the original lifetime
73    pub fn as_ptr<'a>(&self) -> *const Scope<'a> {
74        // SAFETY: Restoring the original lifetime
75        self.0.cast()
76    }
77}
78
79/// Thread-safe wrapper for raw HirNode pointer.
80#[derive(Clone, Copy)]
81pub struct HirNodePtr(*const HirNode<'static>);
82
83unsafe impl Send for HirNodePtr {}
84unsafe impl Sync for HirNodePtr {}
85
86impl HirNodePtr {
87    pub fn new<'a>(ptr: *const HirNode<'a>) -> Self {
88        // SAFETY: We're casting to erase the lifetime, which is intentional
89        Self(ptr.cast())
90    }
91
92    pub fn as_ptr<'a>(&self) -> *const HirNode<'a> {
93        // SAFETY: Restoring the original lifetime
94        self.0.cast()
95    }
96}
97
98#[derive(Debug, Copy, Clone)]
99pub struct CompileUnit<'tcx> {
100    pub cc: &'tcx CompileCtxt<'tcx>,
101    pub index: usize,
102}
103
104impl<'tcx> CompileUnit<'tcx> {
105    pub fn file(&self) -> &'tcx File {
106        &self.cc.files[self.index]
107    }
108
109    /// Get the generic parse tree for this compilation unit
110    pub fn parse_tree(&self) -> Option<&dyn ParseTree> {
111        self.cc
112            .parse_trees
113            .get(self.index)
114            .and_then(|t| t.as_deref())
115    }
116
117    /// Access the shared string interner.
118    pub fn interner(&self) -> &InternPool {
119        &self.cc.interner
120    }
121
122    /// Intern a string and return its symbol.
123    pub fn intern_str<S>(&self, value: S) -> InternedStr
124    where
125        S: AsRef<str>,
126    {
127        self.cc.interner.intern(value)
128    }
129
130    /// Resolve an interned symbol into an owned string.
131    pub fn resolve_interned_owned(&self, symbol: InternedStr) -> Option<String> {
132        self.cc.interner.resolve_owned(symbol)
133    }
134
135    /// Resolve an interned symbol to string reference with "<unnamed>" as default.
136    /// Useful for display purposes where you need a string that lives for 'static or is owned.
137    pub fn resolve_name_or(&self, symbol: InternedStr, default: &str) -> String {
138        self.resolve_interned_owned(symbol)
139            .unwrap_or_else(|| default.to_string())
140    }
141
142    /// Resolve an interned symbol to string, using "<unnamed>" as default if not found.
143    pub fn resolve_name(&self, symbol: InternedStr) -> String {
144        self.resolve_name_or(symbol, "<unnamed>")
145    }
146
147    pub fn file_root_id(&self) -> Option<HirId> {
148        self.cc.file_root_id(self.index)
149    }
150
151    pub fn file_path(&self) -> Option<&str> {
152        self.cc.file_path(self.index)
153    }
154
155    /// Get the module metadata for this compilation unit.
156    /// Contains package/module/file names and roots.
157    pub fn unit_meta(&self) -> &UnitMeta {
158        &self.cc.unit_metas[self.index]
159    }
160
161    /// Reserve a new block ID
162    pub fn reserve_block_id(&self) -> BlockId {
163        BlockId::allocate()
164    }
165
166    /// Get text from the file between start and end byte positions
167    pub fn get_text(&self, start: usize, end: usize) -> String {
168        self.file().get_text(start, end)
169    }
170
171    /// Convenience: extract text for a Tree-sitter node.
172    pub fn ts_text(&self, node: Node<'tcx>) -> String {
173        self.get_text(node.start_byte(), node.end_byte())
174    }
175
176    /// Convenience: extract text for a HIR node.
177    pub fn hir_text(&self, node: &HirNode<'tcx>) -> String {
178        self.get_text(node.start_byte(), node.end_byte())
179    }
180
181    /// Get a HIR node by ID, returning None if not found
182    pub fn opt_hir_node(self, id: HirId) -> Option<HirNode<'tcx>> {
183        self.cc.get_hir_node(id)
184    }
185
186    /// Get a HIR node by ID, panicking if not found
187    pub fn hir_node(self, id: HirId) -> HirNode<'tcx> {
188        self.opt_hir_node(id)
189            .unwrap_or_else(|| panic!("hir node not found {id}"))
190    }
191
192    /// Get a HIR node by ID, returning None if not found
193    pub fn opt_bb(self, id: BlockId) -> Option<BasicBlock<'tcx>> {
194        // Use DashMap-based lookup by ID
195        self.cc.block_arena.get_bb(id.0 as usize).cloned()
196    }
197
198    /// Get a HIR node by ID, panicking if not found
199    pub fn bb(self, id: BlockId) -> BasicBlock<'tcx> {
200        self.opt_bb(id)
201            .unwrap_or_else(|| panic!("basic block not found: {id}"))
202    }
203
204    /// Get the Root block for this compile unit (file).
205    /// Returns the first BlockKind::Root block belonging to this unit.
206    pub fn root_block(self) -> Option<BasicBlock<'tcx>> {
207        let root_blocks = self
208            .cc
209            .find_blocks_by_kind_in_unit(crate::block::BlockKind::Root, self.index);
210        root_blocks.first().and_then(|&id| self.opt_bb(id))
211    }
212
213    /// Get the parent of a HIR node
214    pub fn parent_node(self, id: HirId) -> Option<HirId> {
215        self.opt_hir_node(id).and_then(|node| node.parent())
216    }
217
218    /// Get an existing scope or None if it doesn't exist
219    pub fn opt_get_scope(self, scope_id: ScopeId) -> Option<&'tcx Scope<'tcx>> {
220        self.cc.opt_get_scope(scope_id)
221    }
222
223    /// Get a symbol by ID, delegating to CompileCtxt
224    pub fn opt_get_symbol(self, owner: SymId) -> Option<&'tcx Symbol> {
225        self.cc.opt_get_symbol(owner)
226    }
227
228    /// Get an existing scope or panics if it doesn't exist
229    pub fn get_scope(self, scope_id: ScopeId) -> &'tcx Scope<'tcx> {
230        self.opt_get_scope(scope_id)
231            .expect("ScopeId not mapped to Scope in CompileCtxt")
232    }
233
234    pub fn insert_block(&self, id: BlockId, block: BasicBlock<'tcx>, _parent: BlockId) {
235        // Get block info before allocation
236        let block_kind = block.kind();
237        let block_name = block
238            .base()
239            .and_then(|base| base.opt_get_name())
240            .map(|s| s.to_string());
241
242        // Allocate block into the Arena's DashMap using BlockId as key
243        self.cc.block_arena.alloc_with_id(id.0 as usize, block);
244
245        // Register the block in the index maps (now concurrent-safe)
246        self.cc
247            .block_indexes
248            .insert_block(id, block_name, block_kind, self.index);
249    }
250}
251
252impl<'tcx> Deref for CompileUnit<'tcx> {
253    type Target = CompileCtxt<'tcx>;
254
255    #[inline(always)]
256    fn deref(&self) -> &Self::Target {
257        self.cc
258    }
259}
260
261#[derive(Debug, Clone)]
262pub struct ParentedNode<'tcx> {
263    pub node: HirNode<'tcx>,
264}
265
266impl<'tcx> ParentedNode<'tcx> {
267    pub fn new(node: HirNode<'tcx>) -> Self {
268        Self { node }
269    }
270
271    /// Get a reference to the wrapped node
272    pub fn node(&self) -> &HirNode<'tcx> {
273        &self.node
274    }
275
276    /// Get the parent ID
277    pub fn parent(&self) -> Option<HirId> {
278        self.node.parent()
279    }
280}
281
282#[derive(Debug, Clone, Default)]
283pub struct FileParseMetric {
284    pub path: String,
285    pub seconds: f64,
286}
287
288#[derive(Debug, Clone, Default)]
289pub struct BuildMetrics {
290    pub file_read_seconds: f64,
291    pub parse_wall_seconds: f64,
292    pub parse_cpu_seconds: f64,
293    pub parse_avg_seconds: f64,
294    pub parse_file_count: usize,
295    pub parse_slowest: Vec<FileParseMetric>,
296}
297
298#[derive(Default)]
299pub struct CompileCtxt<'tcx> {
300    pub arena: Arena<'tcx>,
301    pub interner: InternPool,
302    pub files: Vec<File>,
303    /// Per-file metadata: package/module/file names and roots.
304    pub unit_metas: Vec<UnitMeta>,
305    /// Generic parse trees from language-specific parsers
306    pub parse_trees: Vec<Option<Box<dyn ParseTree>>>,
307    pub hir_root_ids: RwLock<Vec<Option<HirId>>>,
308
309    pub block_arena: BlockArena<'tcx>,
310    pub related_map: BlockRelationMap,
311
312    /// Index maps for efficient block lookups by name, kind, unit, and id
313    /// Uses DashMap internally for concurrent access
314    pub block_indexes: BlockIndexMaps,
315
316    /// Metrics collected while building the compilation context
317    pub build_metrics: BuildMetrics,
318}
319
320impl<'tcx> std::fmt::Debug for CompileCtxt<'tcx> {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        f.debug_struct("CompileCtxt")
323            .field("files", &self.files.len())
324            .field("parse_trees", &self.parse_trees.len())
325            .field("build_metrics", &self.build_metrics)
326            .finish()
327    }
328}
329
330impl<'tcx> CompileCtxt<'tcx> {
331    /// Create a new CompileCtxt from source code
332    pub fn from_sources<L: LanguageTrait>(sources: &[Vec<u8>]) -> Self {
333        // Write sources to a unique temporary directory using UUID
334        let temp_dir = std::env::temp_dir()
335            .join("llmcc")
336            .join(Uuid::new_v4().to_string());
337        let _ = fs::create_dir_all(&temp_dir);
338
339        let paths: Vec<String> = sources
340            .iter()
341            .enumerate()
342            .map(|(index, src)| {
343                let path = temp_dir.join(format!("source_{index}.rs"));
344                if let Ok(mut file) = fs::File::create(&path) {
345                    let _ = file.write_all(src);
346                }
347                path.to_string_lossy().to_string()
348            })
349            .collect();
350
351        // Use from_files to parse and build context
352        Self::from_files::<L>(&paths).unwrap_or_else(|_| {
353            // Fallback: create empty context if temp file creation fails
354            Self::default()
355        })
356    }
357
358    /// Create a new CompileCtxt from files with default (original) ordering.
359    pub fn from_files<L: LanguageTrait>(paths: &[String]) -> std::io::Result<Self> {
360        Self::from_files_with_order::<L>(paths, FileOrder::Original)
361    }
362
363    /// Create a new CompileCtxt from files with specified ordering.
364    pub fn from_files_with_order<L: LanguageTrait>(
365        paths: &[String],
366        order: FileOrder,
367    ) -> std::io::Result<Self> {
368        reset_hir_id_counter();
369        reset_symbol_id_counter();
370        reset_scope_id_counter();
371        reset_block_id_counter();
372
373        let read_start = Instant::now();
374
375        let mut files_with_index: Vec<(usize, File)> = paths
376            .par_iter()
377            .enumerate()
378            .map(|(index, path)| -> std::io::Result<(usize, File)> {
379                let file = File::new_file(path.clone())?;
380                Ok((index, file))
381            })
382            .collect::<std::io::Result<Vec<_>>>()?;
383
384        // Sort files based on ordering strategy
385        match order {
386            FileOrder::Original => files_with_index.sort_by_key(|(index, _)| *index),
387            FileOrder::BySizeDescending => {
388                files_with_index.sort_by_key(|(_, file)| std::cmp::Reverse(file.content().len()))
389            }
390        }
391        let files: Vec<File> = files_with_index.into_iter().map(|(_, file)| file).collect();
392
393        let file_read_seconds = read_start.elapsed().as_secs_f64();
394
395        let (parse_trees, mut metrics) = Self::parse_files_with_metrics::<L>(&files);
396        metrics.file_read_seconds = file_read_seconds;
397
398        // Build unit metadata using UnitMetaBuilder
399        let unit_metas = Self::build_unit_metas::<L>(&files);
400        let count = unit_metas.len();
401
402        Ok(Self {
403            arena: Arena::default(),
404            interner: InternPool::default(),
405            files,
406            unit_metas,
407            parse_trees,
408            hir_root_ids: RwLock::new(vec![None; count]),
409            block_arena: BlockArena::default(),
410            related_map: BlockRelationMap::default(),
411            block_indexes: BlockIndexMaps::new(),
412            build_metrics: metrics,
413        })
414    }
415
416    /// Create a new CompileCtxt from files with separate physical and logical paths.
417    /// Physical paths are used to read files from disk; logical paths are stored for display.
418    /// Each element is (physical_path, logical_path).
419    pub fn from_files_with_logical<L: LanguageTrait>(
420        paths: &[(String, String)],
421    ) -> std::io::Result<Self> {
422        Self::from_files_with_logical_and_order::<L>(paths, FileOrder::Original)
423    }
424
425    /// Create a new CompileCtxt from files with separate physical and logical paths and specified ordering.
426    pub fn from_files_with_logical_and_order<L: LanguageTrait>(
427        paths: &[(String, String)],
428        order: FileOrder,
429    ) -> std::io::Result<Self> {
430        reset_hir_id_counter();
431        reset_symbol_id_counter();
432        reset_scope_id_counter();
433        reset_block_id_counter();
434
435        let read_start = Instant::now();
436
437        let mut files_with_index: Vec<(usize, File)> = paths
438            .par_iter()
439            .enumerate()
440            .map(
441                |(index, (physical, logical))| -> std::io::Result<(usize, File)> {
442                    let file = File::new_file_with_logical(physical, logical.clone())?;
443                    Ok((index, file))
444                },
445            )
446            .collect::<std::io::Result<Vec<_>>>()?;
447
448        // Sort files based on ordering strategy
449        match order {
450            FileOrder::Original => files_with_index.sort_by_key(|(index, _)| *index),
451            FileOrder::BySizeDescending => {
452                files_with_index.sort_by_key(|(_, file)| std::cmp::Reverse(file.content().len()))
453            }
454        }
455        let files: Vec<File> = files_with_index.into_iter().map(|(_, file)| file).collect();
456
457        let file_read_seconds = read_start.elapsed().as_secs_f64();
458
459        let (parse_trees, mut metrics) = Self::parse_files_with_metrics::<L>(&files);
460        metrics.file_read_seconds = file_read_seconds;
461
462        // Build unit metadata using UnitMetaBuilder
463        let unit_metas = Self::build_unit_metas::<L>(&files);
464        let count = unit_metas.len();
465
466        Ok(Self {
467            arena: Arena::default(),
468            interner: InternPool::default(),
469            files,
470            unit_metas,
471            parse_trees,
472            hir_root_ids: RwLock::new(vec![None; count]),
473            block_arena: BlockArena::default(),
474            related_map: BlockRelationMap::default(),
475            block_indexes: BlockIndexMaps::new(),
476            build_metrics: metrics,
477        })
478    }
479
480    /// Build unit metadata for all files using UnitMetaBuilder.
481    fn build_unit_metas<L: LanguageTrait>(files: &[File]) -> Vec<UnitMeta> {
482        if files.is_empty() {
483            return Vec::new();
484        }
485
486        // Collect file paths
487        let file_paths: Vec<std::path::PathBuf> = files
488            .iter()
489            .filter_map(|f| f.path().map(std::path::PathBuf::from))
490            .collect();
491
492        if file_paths.is_empty() {
493            return vec![UnitMeta::default(); files.len()];
494        }
495
496        // Build the detector (computes project root internally)
497        let builder = UnitMetaBuilder::from_lang_trait::<L>(&file_paths);
498
499        // Generate metadata for each file
500        let mut metas: Vec<UnitMeta> = files
501            .iter()
502            .map(|f| {
503                if let Some(path) = f.path() {
504                    builder.get_module_info(std::path::Path::new(path))
505                } else {
506                    UnitMeta::default()
507                }
508            })
509            .collect();
510
511        // Assign unique crate_index to each distinct package_root
512        // This enables O(1) same-crate checks during symbol lookup
513        let mut package_root_to_crate_index: std::collections::HashMap<std::path::PathBuf, usize> =
514            std::collections::HashMap::new();
515        let mut next_crate_index = 0usize;
516
517        for meta in &mut metas {
518            if let Some(ref package_root) = meta.package_root {
519                let crate_idx = *package_root_to_crate_index
520                    .entry(package_root.clone())
521                    .or_insert_with(|| {
522                        let idx = next_crate_index;
523                        next_crate_index += 1;
524                        idx
525                    });
526                meta.crate_index = crate_idx;
527            }
528        }
529
530        metas
531    }
532
533    fn parse_files_with_metrics<L: LanguageTrait>(
534        files: &[File],
535    ) -> (Vec<Option<Box<dyn ParseTree>>>, BuildMetrics) {
536        struct ParseRecord {
537            tree: Option<Box<dyn ParseTree>>,
538            elapsed: f64,
539            path: Option<String>,
540        }
541
542        let parse_wall_start = Instant::now();
543        let records: Vec<ParseRecord> = files
544            .par_iter()
545            .map(|file| {
546                let path = file.path().map(|p| p.to_string());
547                let per_file_start = Instant::now();
548                let tree = L::parse(file.content());
549                let elapsed = per_file_start.elapsed().as_secs_f64();
550                ParseRecord {
551                    tree,
552                    elapsed,
553                    path,
554                }
555            })
556            .collect();
557        let parse_wall_seconds = parse_wall_start.elapsed().as_secs_f64();
558
559        let mut trees = Vec::with_capacity(records.len());
560        let parse_file_count = records.len();
561        let mut parse_cpu_seconds = 0.0;
562        let mut slowest = Vec::with_capacity(records.len());
563
564        for record in records {
565            parse_cpu_seconds += record.elapsed;
566            trees.push(record.tree);
567            let path = record.path.unwrap_or_else(|| "<memory>".to_string());
568            slowest.push(FileParseMetric {
569                path,
570                seconds: record.elapsed,
571            });
572        }
573
574        slowest.sort_by(|a, b| {
575            b.seconds
576                .partial_cmp(&a.seconds)
577                .unwrap_or(CmpOrdering::Equal)
578        });
579        slowest.truncate(5);
580
581        let metrics = BuildMetrics {
582            file_read_seconds: 0.0,
583            parse_wall_seconds,
584            parse_cpu_seconds,
585            parse_avg_seconds: if parse_file_count == 0 {
586                0.0
587            } else {
588                parse_cpu_seconds / parse_file_count as f64
589            },
590            parse_file_count,
591            parse_slowest: slowest,
592        };
593
594        (trees, metrics)
595    }
596
597    /// Sentinel owner id reserved for the global scope so that file-level scopes
598    /// (whose HIR id often defaults to 0) do not reuse the same `Scope` instance.
599    pub const GLOBAL_SCOPE_OWNER: HirId = HirId(usize::MAX);
600
601    /// Create a context that references this CompileCtxt for a specific file index
602    pub fn compile_unit(&'tcx self, index: usize) -> CompileUnit<'tcx> {
603        CompileUnit { cc: self, index }
604    }
605
606    pub fn create_unit_globals(&'tcx self, owner: HirId) -> &'tcx Scope<'tcx> {
607        // Create scope (it generates its own ID) then alloc with that ID
608        let scope = Scope::new_with(owner, None, Some(&self.interner));
609        let id = scope.id().0;
610        self.arena.alloc_with_id(id, scope)
611    }
612
613    pub fn create_globals(&'tcx self) -> &'tcx Scope<'tcx> {
614        // Use 256 shards for globals scope - heavily contended during parallel binding
615        let scope =
616            Scope::new_with_shards(Self::GLOBAL_SCOPE_OWNER, None, Some(&self.interner), 256);
617        let id = scope.id().0;
618        self.arena.alloc_with_id(id, scope)
619    }
620
621    pub fn get_scope(&'tcx self, scope_id: ScopeId) -> &'tcx Scope<'tcx> {
622        self.arena
623            .get_scope(scope_id.0)
624            .expect("ScopeId not mapped to Scope in CompileCtxt")
625    }
626
627    pub fn opt_get_scope(&'tcx self, scope_id: ScopeId) -> Option<&'tcx Scope<'tcx>> {
628        // Direct lookup from Arena's DashMap using ID, following redirects if scope was merged
629        self.arena.get_scope(scope_id.0).and_then(|scope| {
630            if let Some(target_id) = scope.get_redirect() {
631                // Follow redirect chain
632                self.opt_get_scope(target_id)
633            } else {
634                Some(scope)
635            }
636        })
637    }
638
639    pub fn opt_get_symbol(&'tcx self, owner: SymId) -> Option<&'tcx Symbol> {
640        self.arena.get_symbol(owner.0)
641    }
642
643    pub fn get_symbol(&'tcx self, owner: SymId) -> &'tcx Symbol {
644        self.opt_get_symbol(owner)
645            .expect("SymId not mapped to Symbol in CompileCtxt")
646    }
647
648    /// Find the primary symbol associated with a block ID
649    pub fn find_symbol_by_block_id(&'tcx self, block_id: BlockId) -> Option<&'tcx Symbol> {
650        self.arena
651            .iter_symbol()
652            .find(|symbol| symbol.block_id() == Some(block_id))
653    }
654
655    /// Access the arena for allocations
656    pub fn arena(&'tcx self) -> &'tcx Arena<'tcx> {
657        &self.arena
658    }
659
660    /// Allocate a new file identifier node with the given ID, name and symbol
661    pub fn alloc_file_ident(
662        &'tcx self,
663        id: HirId,
664        name: &'tcx str,
665        symbol: &'tcx Symbol,
666    ) -> &'tcx HirIdent<'tcx> {
667        let base = HirBase {
668            id,
669            parent: None,
670            kind_id: 0,
671            start_byte: 0,
672            end_byte: 0,
673            kind: HirKind::Identifier,
674            field_id: u16::MAX,
675            children: SmallVec::new(),
676        };
677        let ident = self.arena.alloc(HirIdent::new(base, name));
678        ident.set_symbol(symbol);
679        ident
680    }
681
682    pub fn alloc_scope(&'tcx self, owner: HirId) -> &'tcx Scope<'tcx> {
683        let scope = Scope::new_with(owner, None, Some(&self.interner));
684        let id = scope.id().0;
685        self.arena.alloc_with_id(id, scope)
686    }
687
688    /// Merge the second scope into the first.
689    ///
690    /// This combines all symbols from the second scope into the first scope.
691    /// Any future lookup of second's scope ID will redirect to first.
692    pub fn merge_two_scopes(&'tcx self, first: &'tcx Scope<'tcx>, second: &'tcx Scope<'tcx>) {
693        // Merge symbols from second into first
694        first.merge_with(second, self.arena());
695        // Redirect second's scope ID to first's scope ID so lookups redirect
696        second.set_redirect(first.id());
697    }
698
699    pub fn set_file_root_id(&self, index: usize, start: HirId) {
700        let mut starts = self.hir_root_ids.write();
701        if index < starts.len() && starts[index].is_none() {
702            starts[index] = Some(start);
703        }
704    }
705
706    pub fn file_root_id(&self, index: usize) -> Option<HirId> {
707        self.hir_root_ids.read().get(index).and_then(|opt| *opt)
708    }
709
710    pub fn file_path(&self, index: usize) -> Option<&str> {
711        self.files.get(index).and_then(|file| file.path())
712    }
713
714    /// Get the generic parse tree for a specific file
715    pub fn get_parse_tree(&self, index: usize) -> Option<&dyn ParseTree> {
716        self.parse_trees.get(index).and_then(|t| t.as_deref())
717    }
718
719    /// Get all file paths from the compilation context
720    pub fn get_files(&self) -> Vec<String> {
721        self.files
722            .iter()
723            .filter_map(|f| f.path().map(|p| p.to_string()))
724            .collect()
725    }
726
727    // ========== HIR Map APIs ==========
728
729    /// Get a HIR node by ID from the Arena's DashMap (O(1) concurrent lookup)
730    pub fn get_hir_node(&'tcx self, id: HirId) -> Option<HirNode<'tcx>> {
731        self.arena.get_hir_node(id.0).copied()
732    }
733
734    /// Check if a HIR node exists in the Arena (O(1) check)
735    pub fn hir_node_exists(&self, id: HirId) -> bool {
736        self.arena.hir_node.contains_key(&id.0)
737    }
738
739    /// Get the total count of HIR nodes in the Arena
740    pub fn hir_node_count(&self) -> usize {
741        self.arena.len_hir_node()
742    }
743
744    /// Get all HIR node IDs from the Arena
745    pub fn all_hir_node_ids(&'tcx self) -> Vec<HirId> {
746        self.arena.iter_hir_node().map(|node| node.id()).collect()
747    }
748
749    // ========== Block Indexes APIs ==========
750
751    /// Get all blocks by name
752    pub fn find_blocks_by_name(
753        &self,
754        name: &'tcx str,
755    ) -> Vec<(usize, crate::block::BlockKind, BlockId)> {
756        self.block_indexes.find_by_name(name)
757    }
758
759    /// Get all blocks by kind
760    pub fn find_blocks_by_kind(
761        &self,
762        kind: crate::block::BlockKind,
763    ) -> Vec<(usize, Option<String>, BlockId)> {
764        self.block_indexes.find_by_kind(kind)
765    }
766
767    /// Get blocks in a specific unit
768    pub fn find_blocks_in_unit(
769        &self,
770        unit_index: usize,
771    ) -> Vec<(Option<String>, crate::block::BlockKind, BlockId)> {
772        self.block_indexes.find_by_unit(unit_index)
773    }
774
775    /// Get blocks of a specific kind in a specific unit
776    pub fn find_blocks_by_kind_in_unit(
777        &self,
778        kind: crate::block::BlockKind,
779        unit_index: usize,
780    ) -> Vec<BlockId> {
781        self.block_indexes.find_by_kind_and_unit(kind, unit_index)
782    }
783
784    /// Get block info by ID
785    pub fn get_block_info(
786        &self,
787        block_id: BlockId,
788    ) -> Option<(usize, Option<String>, crate::block::BlockKind)> {
789        self.block_indexes.get_block_info(block_id)
790    }
791
792    /// Get all blocks with their metadata
793    pub fn get_all_blocks(&self) -> Vec<(BlockId, usize, Option<String>, crate::block::BlockKind)> {
794        self.block_indexes.iter_all_blocks()
795    }
796
797    // ========== Symbol Map APIs ==========
798
799    /// Get all symbols from the symbol map
800    pub fn get_all_symbols(&'tcx self) -> Vec<&'tcx Symbol> {
801        self.arena.iter_symbol().collect()
802    }
803
804    /// Get the count of registered symbols (excluding unresolved)
805    pub fn symbol_count(&self) -> usize {
806        self.arena.len_symbol()
807    }
808
809    /// Iterate over all symbols and their IDs (excluding unresolved)
810    pub fn for_each_symbol<F>(&'tcx self, mut f: F)
811    where
812        F: FnMut(SymId, &'tcx Symbol),
813    {
814        for symbol in self.arena.iter_symbol() {
815            f(symbol.id(), symbol);
816        }
817    }
818}