Skip to main content

ghostscope_dwarf/analyzer/
mod.rs

1//! Main DWARF analyzer - unified entry point for all DWARF operations
2
3use crate::{
4    core::{
5        mapping::ModuleMapping, CallerFrameRecovery, DebugInfoSource, ModuleAddress, Result,
6        SectionType, SourceLocation,
7    },
8    loader::ExplicitDebugFile,
9    objfile::LoadedObjfile,
10    semantics::{CompactUnwindRow, CompactUnwindTable, PcContext, VisibleVariable},
11};
12use ghostscope_debuginfod::DebuginfodClient;
13use object::{Object, ObjectSection};
14use std::collections::{HashMap, VecDeque};
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, RwLock};
17
18mod module_resolution;
19mod plan_global;
20mod plan_pc;
21mod source_resolution;
22mod type_lookup;
23
24pub use module_resolution::ModuleDefaultPolicy;
25pub use source_resolution::{SourceLineAddressSearch, SourceLineQuerySearch};
26pub use type_lookup::TypeLookupAmbiguity;
27
28#[cfg(test)]
29use crate::{
30    core::{AddressExpr, Availability, Provenance, VariableLocation},
31    semantics::VariableReadPlan,
32};
33
34/// Events emitted during module loading process
35#[derive(Debug, Clone)]
36pub enum ModuleLoadingEvent {
37    /// Module discovered during process scanning
38    Discovered {
39        module_path: String,
40        current: usize,
41        total: usize,
42    },
43    /// Module loading started
44    LoadingStarted {
45        module_path: String,
46        current: usize,
47        total: usize,
48    },
49    /// Module loading completed successfully
50    LoadingCompleted {
51        module_path: String,
52        stats: ModuleLoadingStats,
53        current: usize,
54        total: usize,
55    },
56    /// Module loading failed
57    LoadingFailed {
58        module_path: String,
59        error: String,
60        current: usize,
61        total: usize,
62    },
63}
64
65/// Statistics for a loaded module
66#[derive(Debug, Clone)]
67pub struct ModuleLoadingStats {
68    pub functions: usize,
69    pub variables: usize,
70    pub types: usize,
71    pub debug_info_source: DebugInfoSource,
72    pub load_time_ms: u64,
73    pub parse_time_ms: u64,
74    pub index_time_ms: u64,
75    pub module_total_time_ms: u64,
76}
77
78/// Rich query result for a single address within a module.
79#[derive(Debug, Clone)]
80pub struct AddressQueryResult {
81    pub module_path: PathBuf,
82    pub address: u64,
83    pub source_file: Option<String>,
84    pub source_line: Option<u32>,
85    pub source_column: Option<u32>,
86    pub function_name: Option<String>,
87    pub is_inline: Option<bool>,
88    pub variables: Vec<VisibleVariable>,
89    pub parameters: Vec<VisibleVariable>,
90}
91
92/// Runtime mapping metadata for a loaded module.
93#[derive(Debug, Clone)]
94pub struct LoadedModuleRuntimeInfo {
95    pub module_path: PathBuf,
96    pub loaded_address: Option<u64>,
97    pub load_bias: Option<u64>,
98    pub size: u64,
99}
100
101/// Rich query result for a function lookup across modules.
102#[derive(Debug, Clone)]
103pub struct FunctionQueryResult {
104    pub function_name: String,
105    pub addresses: Vec<AddressQueryResult>,
106}
107
108/// DWARF analyzer - unified entry point for all DWARF analysis
109#[derive(Debug)]
110pub struct DwarfAnalyzer {
111    /// Process ID
112    pid: u32,
113    /// Module path -> module data mapping
114    modules: HashMap<PathBuf, LoadedObjfile>,
115    /// Cached PC semantic contexts for repeated symbol/source lookups.
116    pc_context_cache: RwLock<PcContextCache>,
117}
118
119const PC_CONTEXT_CACHE_MAX_ENTRIES: usize = 8192;
120
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122struct PcContextCacheKey {
123    module_path: PathBuf,
124    address: u64,
125}
126
127#[derive(Debug)]
128struct PcContextCache {
129    entries: HashMap<PathBuf, HashMap<u64, PcContext>>,
130    insertion_order: VecDeque<PcContextCacheKey>,
131    len: usize,
132    max_entries: usize,
133}
134
135impl Default for PcContextCache {
136    fn default() -> Self {
137        Self {
138            entries: HashMap::new(),
139            insertion_order: VecDeque::new(),
140            len: 0,
141            max_entries: PC_CONTEXT_CACHE_MAX_ENTRIES,
142        }
143    }
144}
145
146impl PcContextCache {
147    fn get(&self, module_path: &Path, address: u64) -> Option<PcContext> {
148        self.entries
149            .get(module_path)
150            .and_then(|entries| entries.get(&address))
151            .cloned()
152    }
153
154    fn insert(&mut self, module_path: PathBuf, address: u64, context: PcContext) {
155        if self.max_entries == 0 {
156            return;
157        }
158
159        let key = PcContextCacheKey {
160            module_path,
161            address,
162        };
163        let module_entries = self.entries.entry(key.module_path.clone()).or_default();
164        if module_entries.insert(address, context).is_none() {
165            self.insertion_order.push_back(key.clone());
166            self.len += 1;
167        }
168
169        while self.len > self.max_entries {
170            let Some(expired) = self.insertion_order.pop_front() else {
171                break;
172            };
173            if let Some(module_entries) = self.entries.get_mut(&expired.module_path) {
174                if module_entries.remove(&expired.address).is_some() {
175                    self.len -= 1;
176                }
177                if module_entries.is_empty() {
178                    self.entries.remove(&expired.module_path);
179                }
180            }
181        }
182    }
183}
184
185impl DwarfAnalyzer {
186    fn build_address_query_result(
187        &self,
188        module_address: &ModuleAddress,
189    ) -> Result<AddressQueryResult> {
190        self.build_address_query_result_with_source_hint(module_address, None)
191    }
192
193    fn build_address_query_result_with_source_hint(
194        &self,
195        module_address: &ModuleAddress,
196        source_hint: Option<(&str, u32)>,
197    ) -> Result<AddressQueryResult> {
198        let mut variables = Vec::new();
199        let mut parameters = Vec::new();
200
201        for variable in self.visible_variables_at_address(module_address)? {
202            if variable.is_parameter {
203                parameters.push(variable);
204            } else {
205                variables.push(variable);
206            }
207        }
208
209        let source_location = if let Some((file_path, line_number)) = source_hint {
210            self.modules
211                .get(&module_address.module_path)
212                .and_then(|module_data| {
213                    module_data.lookup_source_location_for_source_line(
214                        module_address.address,
215                        file_path,
216                        line_number,
217                    )
218                })
219        } else {
220            self.lookup_source_location(module_address)
221        };
222        let function_name = self.find_function_name_by_module_address(module_address);
223        let is_inline = self.is_inline_at(module_address);
224
225        Ok(AddressQueryResult {
226            module_path: module_address.module_path.clone(),
227            address: module_address.address,
228            source_file: source_location.as_ref().map(|sl| sl.file_path.clone()),
229            source_line: source_location.as_ref().map(|sl| sl.line_number),
230            source_column: source_location.as_ref().and_then(|sl| sl.column),
231            function_name,
232            is_inline,
233            variables,
234            parameters,
235        })
236    }
237
238    fn query_module_addresses(
239        &self,
240        module_addresses: Vec<ModuleAddress>,
241    ) -> Result<Vec<AddressQueryResult>> {
242        module_addresses
243            .iter()
244            .map(|module_address| self.build_address_query_result(module_address))
245            .collect()
246    }
247
248    fn query_module_addresses_for_source_line(
249        &self,
250        module_addresses: Vec<ModuleAddress>,
251        file_path: &str,
252        line_number: u32,
253    ) -> Result<Vec<AddressQueryResult>> {
254        module_addresses
255            .iter()
256            .map(|module_address| {
257                self.build_address_query_result_with_source_hint(
258                    module_address,
259                    Some((file_path, line_number)),
260                )
261            })
262            .collect()
263    }
264
265    fn query_module_addresses_best_effort(
266        &self,
267        module_addresses: Vec<ModuleAddress>,
268        query_label: &str,
269    ) -> Result<Vec<AddressQueryResult>> {
270        let mut results = Vec::new();
271        let mut first_error: Option<(ModuleAddress, String)> = None;
272
273        for module_address in &module_addresses {
274            match self.build_address_query_result(module_address) {
275                Ok(result) => results.push(result),
276                Err(error) => {
277                    let error_string = error.to_string();
278                    tracing::warn!(
279                        "Skipping failed address query for {} at {}:0x{:x}: {}",
280                        query_label,
281                        module_address.module_display(),
282                        module_address.address,
283                        error_string
284                    );
285
286                    if first_error.is_none() {
287                        first_error = Some((module_address.clone(), error_string));
288                    }
289                }
290            }
291        }
292
293        if results.is_empty() {
294            if let Some((module_address, error)) = first_error {
295                return Err(anyhow::anyhow!(
296                    "Failed to analyze any address for {} (first failure at {}:0x{:x}: {})",
297                    query_label,
298                    module_address.module_display(),
299                    module_address.address,
300                    error
301                ));
302            }
303        }
304
305        Ok(results)
306    }
307
308    fn query_module_addresses_for_source_line_best_effort(
309        &self,
310        module_addresses: Vec<ModuleAddress>,
311        file_path: &str,
312        line_number: u32,
313        query_label: &str,
314    ) -> Result<Vec<AddressQueryResult>> {
315        let mut results = Vec::new();
316        let mut first_error: Option<(ModuleAddress, String)> = None;
317
318        for module_address in &module_addresses {
319            match self.build_address_query_result_with_source_hint(
320                module_address,
321                Some((file_path, line_number)),
322            ) {
323                Ok(result) => results.push(result),
324                Err(error) => {
325                    let error_string = error.to_string();
326                    tracing::warn!(
327                        "Skipping failed address query for {} at {}:0x{:x}: {}",
328                        query_label,
329                        module_address.module_display(),
330                        module_address.address,
331                        error_string
332                    );
333
334                    if first_error.is_none() {
335                        first_error = Some((module_address.clone(), error_string));
336                    }
337                }
338            }
339        }
340
341        if results.is_empty() {
342            if let Some((module_address, error)) = first_error {
343                return Err(anyhow::anyhow!(
344                    "Failed to analyze any address for {} (first failure at {}:0x{:x}: {})",
345                    query_label,
346                    module_address.module_display(),
347                    module_address.address,
348                    error
349                ));
350            }
351        }
352
353        Ok(results)
354    }
355
356    fn find_function_name_by_module_address(
357        &self,
358        module_address: &ModuleAddress,
359    ) -> Option<String> {
360        self.loaded_module_path_for(&module_address.module_path)
361            .and_then(|module_path| self.modules.get(module_path))
362            .and_then(|module_data| {
363                module_data.find_function_name_by_address(module_address.address)
364            })
365    }
366
367    fn sorted_module_paths(&self) -> Vec<&PathBuf> {
368        let mut paths: Vec<&PathBuf> = self.modules.keys().collect();
369        paths.sort();
370        paths
371    }
372
373    pub(crate) fn loaded_module_path_for<P: AsRef<Path>>(
374        &self,
375        module_path: P,
376    ) -> Option<&PathBuf> {
377        let module_path = module_path.as_ref();
378        if let Some((path, _)) = self.modules.get_key_value(module_path) {
379            return Some(path);
380        }
381
382        self.sorted_module_paths()
383            .into_iter()
384            .find(|path| Self::module_paths_equivalent(path.as_path(), module_path))
385    }
386
387    /// Return the deterministic per-analyzer module id for a loaded module path.
388    pub fn module_id_for_path<P: AsRef<Path>>(&self, module_path: P) -> Option<crate::ModuleId> {
389        let module_path = self.loaded_module_path_for(module_path)?;
390        self.sorted_module_paths()
391            .into_iter()
392            .position(|path| path.as_path() == module_path.as_path())
393            .map(|index| crate::ModuleId(index as u32))
394    }
395
396    /// Resolve a semantic module id back to its loaded module path.
397    pub fn module_path_for_id(&self, module: crate::ModuleId) -> Option<&Path> {
398        self.sorted_module_paths()
399            .get(module.0 as usize)
400            .map(|path| path.as_path())
401    }
402
403    /// Create DWARF analyzer from PID (now uses parallel loading)
404    pub async fn from_pid(pid: u32) -> Result<Self> {
405        Self::from_pid_parallel(pid).await
406    }
407
408    /// Classify whether an address is inside an inlined subroutine instance
409    /// Returns Some(true) if inline, Some(false) if a normal (non-inline) context,
410    /// or None if the module/address cannot be resolved.
411    pub fn is_inline_at(&self, module_address: &ModuleAddress) -> Option<bool> {
412        if let Some(module_data) = self
413            .loaded_module_path_for(&module_address.module_path)
414            .and_then(|module_path| self.modules.get(module_path))
415        {
416            module_data.is_inline_at(module_address.address)
417        } else {
418            None
419        }
420    }
421
422    /// Create DWARF analyzer from PID using parallel loading
423    pub async fn from_pid_parallel(pid: u32) -> Result<Self> {
424        Self::from_pid_parallel_with_config(pid, &[], false, |_event| {}).await
425    }
426
427    /// Create DWARF analyzer from PID using parallel loading with progress callback
428    pub async fn from_pid_parallel_with_progress<F>(pid: u32, progress_callback: F) -> Result<Self>
429    where
430        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
431    {
432        Self::from_pid_parallel_with_config(pid, &[], false, progress_callback).await
433    }
434
435    /// Create DWARF analyzer from PID using parallel loading with debug search paths and progress callback
436    pub async fn from_pid_parallel_with_config<F>(
437        pid: u32,
438        debug_search_paths: &[String],
439        allow_loose_debug_match: bool,
440        progress_callback: F,
441    ) -> Result<Self>
442    where
443        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
444    {
445        Self::from_pid_parallel_with_config_and_debuginfod(
446            pid,
447            debug_search_paths,
448            allow_loose_debug_match,
449            None,
450            progress_callback,
451        )
452        .await
453    }
454
455    /// Create DWARF analyzer from PID with debug search paths, debuginfod, and progress callback.
456    pub async fn from_pid_parallel_with_config_and_debuginfod<F>(
457        pid: u32,
458        debug_search_paths: &[String],
459        allow_loose_debug_match: bool,
460        debuginfod_client: Option<Arc<DebuginfodClient>>,
461        progress_callback: F,
462    ) -> Result<Self>
463    where
464        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
465    {
466        tracing::info!("Creating DWARF analyzer for PID {} (parallel)", pid);
467
468        let module_runtime_info = Self::discover_pid_runtime_modules(pid)?;
469
470        Self::from_pid_runtime_modules_with_config_and_debuginfod(
471            pid,
472            module_runtime_info,
473            debug_search_paths,
474            allow_loose_debug_match,
475            debuginfod_client,
476            progress_callback,
477        )
478        .await
479    }
480
481    /// Discover runtime module mappings for a PID.
482    pub fn discover_pid_runtime_modules(pid: u32) -> Result<Vec<LoadedModuleRuntimeInfo>> {
483        let mut coord = ghostscope_process::ProcessManager::new();
484        coord.ensure_prefill_pid(pid)?;
485        Ok(coord
486            .cached_offsets_with_paths_for_pid(pid)
487            .map(Self::runtime_modules_from_pid_offsets)
488            .unwrap_or_default())
489    }
490
491    /// Convert cached process offsets into one runtime mapping per module path.
492    pub fn runtime_modules_from_pid_offsets(
493        entries: &[ghostscope_process::PidOffsetsEntry],
494    ) -> Vec<LoadedModuleRuntimeInfo> {
495        let mut seen = std::collections::HashSet::new();
496        entries
497            .iter()
498            .filter(|entry| seen.insert(entry.module_path.clone()))
499            .map(|entry| LoadedModuleRuntimeInfo {
500                module_path: PathBuf::from(&entry.module_path),
501                loaded_address: Some(entry.base),
502                load_bias: Some(entry.offsets.text),
503                size: entry.size,
504            })
505            .collect()
506    }
507
508    fn runtime_modules_to_module_mappings(
509        runtime_modules: Vec<LoadedModuleRuntimeInfo>,
510    ) -> Vec<ModuleMapping> {
511        runtime_modules
512            .into_iter()
513            .map(|module| {
514                let mut mapping = ModuleMapping::from_path(module.module_path);
515                mapping.loaded_address = module.loaded_address;
516                mapping.load_bias = module.load_bias;
517                mapping.size = module.size;
518                mapping
519            })
520            .collect()
521    }
522
523    /// Load newly discovered modules into an existing PID analyzer.
524    pub async fn refresh_pid_runtime_modules_with_config_and_debuginfod<F>(
525        &mut self,
526        runtime_modules: Vec<LoadedModuleRuntimeInfo>,
527        debug_search_paths: &[String],
528        allow_loose_debug_match: bool,
529        debuginfod_client: Option<Arc<DebuginfodClient>>,
530        progress_callback: F,
531    ) -> Result<usize>
532    where
533        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
534    {
535        let mut new_runtime_modules = Vec::new();
536        let mut updated_existing = 0usize;
537
538        for runtime_module in runtime_modules {
539            let existing = self.modules.iter_mut().find(|(path, _)| {
540                Self::module_paths_equivalent(path.as_path(), &runtime_module.module_path)
541            });
542
543            if let Some((_path, loaded)) = existing {
544                let mapping = loaded.module_mapping();
545                if mapping.loaded_address != runtime_module.loaded_address
546                    || mapping.load_bias != runtime_module.load_bias
547                    || mapping.size != runtime_module.size
548                {
549                    loaded.update_runtime_mapping(
550                        runtime_module.loaded_address,
551                        runtime_module.load_bias,
552                        runtime_module.size,
553                    );
554                    updated_existing += 1;
555                }
556            } else {
557                new_runtime_modules.push(runtime_module);
558            }
559        }
560
561        if updated_existing > 0 {
562            self.clear_pc_context_cache();
563            tracing::debug!(
564                "Updated runtime mapping metadata for {} loaded module(s)",
565                updated_existing
566            );
567        }
568
569        if new_runtime_modules.is_empty() {
570            return Ok(0);
571        }
572
573        tracing::info!(
574            "Refreshing DWARF analyzer for PID {} with {} newly mapped module(s)",
575            self.pid,
576            new_runtime_modules.len()
577        );
578
579        let module_mappings = Self::runtime_modules_to_module_mappings(new_runtime_modules);
580
581        for (index, mapping) in module_mappings.iter().enumerate() {
582            progress_callback(ModuleLoadingEvent::Discovered {
583                module_path: mapping.path.to_string_lossy().to_string(),
584                current: index + 1,
585                total: module_mappings.len(),
586            });
587        }
588
589        let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
590        if !debug_search_paths.is_empty() {
591            loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
592        }
593        loader = loader.with_loose_debug_match(allow_loose_debug_match);
594        loader = loader.with_debuginfod_client(debuginfod_client);
595
596        let modules = loader
597            .with_progress_callback(progress_callback)
598            .load()
599            .await?;
600        let loaded_count = modules.len();
601
602        for module in modules {
603            let module_path = module.module_path().clone();
604            self.modules.insert(module_path, module);
605        }
606
607        if loaded_count > 0 {
608            self.clear_pc_context_cache();
609            tracing::info!(
610                "DWARF analyzer for PID {} loaded {} new module(s)",
611                self.pid,
612                loaded_count
613            );
614        }
615
616        Ok(loaded_count)
617    }
618
619    /// Create DWARF analyzer from an already discovered PID runtime module snapshot.
620    pub async fn from_pid_runtime_modules_with_config_and_debuginfod<F>(
621        pid: u32,
622        runtime_modules: Vec<LoadedModuleRuntimeInfo>,
623        debug_search_paths: &[String],
624        allow_loose_debug_match: bool,
625        debuginfod_client: Option<Arc<DebuginfodClient>>,
626        progress_callback: F,
627    ) -> Result<Self>
628    where
629        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
630    {
631        Self::from_pid_runtime_modules_with_config_debuginfod_and_explicit_debug_file(
632            pid,
633            runtime_modules,
634            debug_search_paths,
635            allow_loose_debug_match,
636            debuginfod_client,
637            None,
638            progress_callback,
639        )
640        .await
641    }
642
643    /// Create DWARF analyzer from an already discovered PID runtime module snapshot,
644    /// with optional debuginfod and a user-provided debug file for one module.
645    pub async fn from_pid_runtime_modules_with_config_debuginfod_and_explicit_debug_file<F>(
646        pid: u32,
647        runtime_modules: Vec<LoadedModuleRuntimeInfo>,
648        debug_search_paths: &[String],
649        allow_loose_debug_match: bool,
650        debuginfod_client: Option<Arc<DebuginfodClient>>,
651        explicit_debug_file: Option<ExplicitDebugFile>,
652        progress_callback: F,
653    ) -> Result<Self>
654    where
655        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
656    {
657        tracing::info!(
658            "Creating DWARF analyzer for PID {} from {} runtime module mappings",
659            pid,
660            runtime_modules.len()
661        );
662
663        let module_mappings = Self::runtime_modules_to_module_mappings(runtime_modules);
664
665        tracing::info!(
666            "Discovered {} modules for PID {}",
667            module_mappings.len(),
668            pid
669        );
670
671        // Notify discovery completion
672        for (index, mapping) in module_mappings.iter().enumerate() {
673            progress_callback(ModuleLoadingEvent::Discovered {
674                module_path: mapping.path.to_string_lossy().to_string(),
675                current: index + 1,
676                total: module_mappings.len(),
677            });
678        }
679
680        // Load all modules in parallel with progress tracking
681        let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
682
683        // Configure debug search paths if provided
684        if !debug_search_paths.is_empty() {
685            loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
686        }
687        loader = loader.with_loose_debug_match(allow_loose_debug_match);
688        loader = loader.with_explicit_debug_file(explicit_debug_file);
689        loader = loader.with_debuginfod_client(debuginfod_client);
690
691        let modules = loader
692            .with_progress_callback(progress_callback)
693            .load()
694            .await?;
695
696        tracing::info!(
697            "Created DWARF analyzer for PID {} with {} modules (parallel)",
698            pid,
699            modules.len()
700        );
701
702        Ok(Self::from_modules(pid, modules))
703    }
704
705    /// Create DWARF analyzer from executable path (single module mode, now async parallel)
706    pub async fn from_exec_path<P: AsRef<std::path::Path>>(exec_path: P) -> Result<Self> {
707        Self::from_exec_path_with_config(exec_path, &[], false).await
708    }
709
710    /// Create DWARF analyzer from executable path with debug search paths
711    pub async fn from_exec_path_with_config<P: AsRef<std::path::Path>>(
712        exec_path: P,
713        debug_search_paths: &[String],
714        allow_loose_debug_match: bool,
715    ) -> Result<Self> {
716        Self::from_exec_path_with_config_and_debuginfod(
717            exec_path,
718            debug_search_paths,
719            allow_loose_debug_match,
720            None,
721        )
722        .await
723    }
724
725    /// Create DWARF analyzer from executable path with debug search paths and debuginfod.
726    pub async fn from_exec_path_with_config_and_debuginfod<P: AsRef<std::path::Path>>(
727        exec_path: P,
728        debug_search_paths: &[String],
729        allow_loose_debug_match: bool,
730        debuginfod_client: Option<Arc<DebuginfodClient>>,
731    ) -> Result<Self> {
732        Self::from_exec_path_with_config_and_debuginfod_and_progress(
733            exec_path,
734            debug_search_paths,
735            allow_loose_debug_match,
736            debuginfod_client,
737            |_event| {},
738        )
739        .await
740    }
741
742    /// Create DWARF analyzer from executable path with debug search paths and progress callback
743    pub async fn from_exec_path_with_config_and_progress<P, F>(
744        exec_path: P,
745        debug_search_paths: &[String],
746        allow_loose_debug_match: bool,
747        progress_callback: F,
748    ) -> Result<Self>
749    where
750        P: AsRef<std::path::Path>,
751        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
752    {
753        Self::from_exec_path_with_config_and_debuginfod_and_progress(
754            exec_path,
755            debug_search_paths,
756            allow_loose_debug_match,
757            None,
758            progress_callback,
759        )
760        .await
761    }
762
763    /// Create DWARF analyzer from executable path with debug search paths, debuginfod, and progress callback.
764    pub async fn from_exec_path_with_config_and_debuginfod_and_progress<P, F>(
765        exec_path: P,
766        debug_search_paths: &[String],
767        allow_loose_debug_match: bool,
768        debuginfod_client: Option<Arc<DebuginfodClient>>,
769        progress_callback: F,
770    ) -> Result<Self>
771    where
772        P: AsRef<std::path::Path>,
773        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
774    {
775        Self::from_exec_path_with_config_debuginfod_explicit_debug_file_and_progress(
776            exec_path,
777            debug_search_paths,
778            allow_loose_debug_match,
779            debuginfod_client,
780            None,
781            progress_callback,
782        )
783        .await
784    }
785
786    /// Create DWARF analyzer from executable path with debug search paths,
787    /// debuginfod, an optional explicit debug file, and progress callback.
788    pub async fn from_exec_path_with_config_debuginfod_explicit_debug_file_and_progress<P, F>(
789        exec_path: P,
790        debug_search_paths: &[String],
791        allow_loose_debug_match: bool,
792        debuginfod_client: Option<Arc<DebuginfodClient>>,
793        explicit_debug_file: Option<PathBuf>,
794        progress_callback: F,
795    ) -> Result<Self>
796    where
797        P: AsRef<std::path::Path>,
798        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
799    {
800        let exec_path = exec_path.as_ref().to_path_buf();
801        tracing::info!(
802            "Creating DWARF analyzer for executable: {}",
803            exec_path.display()
804        );
805
806        let mut analyzer = Self {
807            pid: 0, // No specific PID in exec mode
808            modules: HashMap::new(),
809            pc_context_cache: RwLock::new(PcContextCache::default()),
810        };
811
812        // Create a single module mapping for the executable
813        // No loaded address since we're not analyzing a running process
814        let module_mapping = ModuleMapping {
815            path: exec_path.clone(),
816            loaded_address: None, // No process mapping in exec path mode
817            load_bias: None,
818            size: 0, // Will be determined from file size if needed
819        };
820        let module_path = exec_path.to_string_lossy().to_string();
821
822        progress_callback(ModuleLoadingEvent::Discovered {
823            module_path: module_path.clone(),
824            current: 1,
825            total: 1,
826        });
827        progress_callback(ModuleLoadingEvent::LoadingStarted {
828            module_path: module_path.clone(),
829            current: 1,
830            total: 1,
831        });
832
833        // Load the single module using parallel loading
834        let start_time = std::time::Instant::now();
835        match LoadedObjfile::load_parallel(
836            module_mapping,
837            debug_search_paths,
838            allow_loose_debug_match,
839            explicit_debug_file,
840            debuginfod_client,
841        )
842        .await
843        {
844            Ok(module_data) => {
845                let (functions, variables, types) = module_data.get_lightweight_index().get_stats();
846                let (parse_time_ms, index_time_ms, module_total_time_ms) =
847                    module_data.get_load_timing_ms();
848                progress_callback(ModuleLoadingEvent::LoadingCompleted {
849                    module_path,
850                    stats: ModuleLoadingStats {
851                        functions,
852                        variables,
853                        types,
854                        debug_info_source: module_data.get_debug_info_source().clone(),
855                        load_time_ms: start_time.elapsed().as_millis() as u64,
856                        parse_time_ms,
857                        index_time_ms,
858                        module_total_time_ms,
859                    },
860                    current: 1,
861                    total: 1,
862                });
863                analyzer.modules.insert(exec_path.clone(), module_data);
864                tracing::info!(
865                    "Created DWARF analyzer for executable {} with 1 module",
866                    exec_path.display()
867                );
868            }
869            Err(e) => {
870                progress_callback(ModuleLoadingEvent::LoadingFailed {
871                    module_path,
872                    error: e.to_string(),
873                    current: 1,
874                    total: 1,
875                });
876                return Err(crate::DwarfError::ModuleLoadError(format!(
877                    "Failed to load executable {}: {}",
878                    exec_path.display(),
879                    e
880                ))
881                .into());
882            }
883        }
884
885        Ok(analyzer)
886    }
887
888    /// Create analyzer from pre-loaded modules (for Builder pattern)
889    pub(crate) fn from_modules(pid: u32, modules: Vec<LoadedObjfile>) -> Self {
890        let mut analyzer = Self {
891            pid,
892            modules: HashMap::new(),
893            pc_context_cache: RwLock::new(PcContextCache::default()),
894        };
895
896        for module in modules {
897            let module_path = module.module_path().clone();
898            analyzer.modules.insert(module_path, module);
899        }
900
901        tracing::info!(
902            "Created DWARF analyzer for PID {} with {} pre-loaded modules",
903            pid,
904            analyzer.modules.len()
905        );
906
907        analyzer
908    }
909
910    fn clear_pc_context_cache(&self) {
911        if let Ok(mut cache) = self.pc_context_cache.write() {
912            *cache = PcContextCache::default();
913        }
914    }
915
916    /// Lookup function addresses across all modules
917    /// Returns: Vec<ModuleAddress> - one for each address where the function is found
918    pub fn lookup_function_addresses(&self, name: &str) -> Vec<ModuleAddress> {
919        let mut results = Vec::new();
920
921        for (module_path, module_data) in &self.modules {
922            let addresses = module_data.lookup_function_addresses_any(name);
923
924            // Create a ModuleAddress for each address found in this module
925            for address in addresses {
926                tracing::debug!(
927                    "Function '{}' found in module {} at address: 0x{:x}",
928                    name,
929                    module_path.display(),
930                    address
931                );
932                results.push(ModuleAddress::new(module_path.clone(), address));
933            }
934        }
935
936        // Deterministic ordering: module path asc, then address asc
937        results.sort_by(|a, b| {
938            let pa = a.module_path.to_string_lossy();
939            let pb = b.module_path.to_string_lossy();
940            match pa.cmp(&pb) {
941                std::cmp::Ordering::Equal => a.address.cmp(&b.address),
942                other => other,
943            }
944        });
945        results
946    }
947
948    /// Query function debug information across all modules.
949    pub fn query_function(&self, name: &str) -> Result<FunctionQueryResult> {
950        let module_addresses = self.lookup_function_addresses(name);
951        let addresses = self.query_module_addresses(module_addresses)?;
952        Ok(FunctionQueryResult {
953            function_name: name.to_string(),
954            addresses,
955        })
956    }
957
958    /// Query function debug information across all modules, skipping addresses
959    /// that fail to resolve so callers can still display partial results.
960    pub fn query_function_best_effort(&self, name: &str) -> Result<FunctionQueryResult> {
961        let module_addresses = self.lookup_function_addresses(name);
962        let addresses = self
963            .query_module_addresses_best_effort(module_addresses, &format!("function '{name}'"))?;
964        Ok(FunctionQueryResult {
965            function_name: name.to_string(),
966            addresses,
967        })
968    }
969
970    /// Convert a module-relative virtual address (DWARF PC) to an ELF file offset
971    /// Returns None if the module is unknown or the address is not within a PT_LOAD segment
972    pub fn vaddr_to_file_offset<P: AsRef<std::path::Path>>(
973        &self,
974        module_path: P,
975        vaddr: u64,
976    ) -> Option<u64> {
977        let path_buf = module_path.as_ref().to_path_buf();
978        if let Some(module_data) = self.modules.get(&path_buf) {
979            module_data.vaddr_to_file_offset(vaddr)
980        } else {
981            None
982        }
983    }
984
985    /// Recover the direct caller frame at a module address as PlanExprOp[].
986    pub fn recover_caller_frame(
987        &self,
988        module_address: &ModuleAddress,
989        registers: &[u16],
990    ) -> Result<Option<CallerFrameRecovery>> {
991        if let Some(module_data) = self
992            .loaded_module_path_for(&module_address.module_path)
993            .and_then(|module_path| self.modules.get(module_path))
994        {
995            module_data.recover_caller_frame(module_address.address, registers)
996        } else {
997            Ok(None)
998        }
999    }
1000
1001    /// Recover the direct caller frame at a previously resolved PC context.
1002    pub fn recover_caller_frame_for_context(
1003        &self,
1004        ctx: &PcContext,
1005        registers: &[u16],
1006    ) -> Result<Option<CallerFrameRecovery>> {
1007        let module_address = self.module_address_for_context(ctx)?;
1008        self.recover_caller_frame(&module_address, registers)
1009    }
1010
1011    /// Build compact unwind rows for the module referenced by a PC context.
1012    pub fn compact_unwind_table_for_context(
1013        &self,
1014        ctx: &PcContext,
1015    ) -> Result<Option<Arc<CompactUnwindTable>>> {
1016        let module_path = self
1017            .module_path_for_id(ctx.module)
1018            .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?;
1019        self.modules
1020            .get(module_path)
1021            .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
1022            .compact_unwind_table(ctx.module)
1023    }
1024
1025    /// Resolve the compact unwind row that covers a previously resolved PC context.
1026    pub fn compact_unwind_row_for_context(
1027        &self,
1028        ctx: &PcContext,
1029    ) -> Result<Option<CompactUnwindRow>> {
1030        let module_path = self
1031            .module_path_for_id(ctx.module)
1032            .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?;
1033        self.modules
1034            .get(module_path)
1035            .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
1036            .compact_unwind_row(ctx.module, ctx.normalized_pc)
1037    }
1038
1039    /// Build compact unwind rows for a loaded semantic module id.
1040    pub fn compact_unwind_table_for_module(
1041        &self,
1042        module: crate::ModuleId,
1043    ) -> Result<Option<Arc<CompactUnwindTable>>> {
1044        let module_path = self
1045            .module_path_for_id(module)
1046            .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", module))?;
1047        self.modules
1048            .get(module_path)
1049            .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
1050            .compact_unwind_table(module)
1051    }
1052
1053    /// Get all loaded module paths
1054    pub fn get_loaded_modules(&self) -> Vec<&PathBuf> {
1055        self.modules.keys().collect()
1056    }
1057
1058    /// Get loaded module paths with process mapping metadata, when available.
1059    pub fn loaded_module_runtime_info(&self) -> Vec<LoadedModuleRuntimeInfo> {
1060        let mut modules: Vec<_> = self
1061            .modules
1062            .values()
1063            .map(|module| {
1064                let mapping = module.module_mapping();
1065                LoadedModuleRuntimeInfo {
1066                    module_path: mapping.path.clone(),
1067                    loaded_address: mapping.loaded_address,
1068                    load_bias: mapping.load_bias,
1069                    size: mapping.size,
1070                }
1071            })
1072            .collect();
1073        modules.sort_by(|left, right| left.module_path.cmp(&right.module_path));
1074        modules
1075    }
1076
1077    /// Get the ELF entry address for a loaded module, when present.
1078    pub fn module_entry_address<P: AsRef<Path>>(&self, module_path: P) -> Option<u64> {
1079        self.modules
1080            .get(module_path.as_ref())
1081            .and_then(|module| module.entry_address())
1082    }
1083
1084    /// Classify the section type for a link-time virtual address in a specific module
1085    pub fn classify_section_for_address<P: AsRef<Path>>(
1086        &self,
1087        module_path: P,
1088        vaddr: u64,
1089    ) -> Option<SectionType> {
1090        let path = module_path.as_ref();
1091        if let Some(module_data) = self.modules.get(path) {
1092            module_data.classify_section_for_vaddr(vaddr)
1093        } else {
1094            None
1095        }
1096    }
1097
1098    /// Lookup function address by name - returns first match
1099    /// Returns ModuleAddress for the first function found
1100    pub fn lookup_function_address_by_name(&self, function_name: &str) -> Option<ModuleAddress> {
1101        let module_addresses = self.lookup_function_addresses(function_name);
1102
1103        if let Some(first_module_address) = module_addresses.first() {
1104            tracing::info!(
1105                "Found function '{}' in module '{}' at address 0x{:x}",
1106                function_name,
1107                first_module_address.module_display(),
1108                first_module_address.address
1109            );
1110            Some(first_module_address.clone())
1111        } else {
1112            tracing::warn!("Function '{}' not found in any module", function_name);
1113            None
1114        }
1115    }
1116
1117    /// Lookup source location by module address
1118    /// Returns source location for the given module address
1119    pub fn lookup_source_location(&self, module_address: &ModuleAddress) -> Option<SourceLocation> {
1120        if let Some(module_data) = self
1121            .loaded_module_path_for(&module_address.module_path)
1122            .and_then(|module_path| self.modules.get(module_path))
1123        {
1124            module_data.lookup_source_location(module_address.address)
1125        } else {
1126            tracing::warn!("Module {} not found", module_address.module_display());
1127            None
1128        }
1129    }
1130
1131    /// Lookup addresses by source line (cross-module)
1132    /// Returns: Vec<ModuleAddress> for all matches
1133    pub fn lookup_addresses_by_source_line(
1134        &self,
1135        file_path: &str,
1136        line_number: u32,
1137    ) -> Vec<ModuleAddress> {
1138        let mut results = Vec::new();
1139
1140        // Check each module for this source:line combination
1141        for (module_path, module_data) in &self.modules {
1142            let addresses = module_data.lookup_addresses_by_source_line(file_path, line_number);
1143
1144            // Add all addresses from this module
1145            for address in addresses {
1146                results.push(ModuleAddress::new(module_path.clone(), address));
1147            }
1148        }
1149
1150        if !results.is_empty() {
1151            tracing::info!(
1152                "Found {} addresses for {}:{} across {} modules",
1153                results.len(),
1154                file_path,
1155                line_number,
1156                self.modules.len()
1157            );
1158        }
1159
1160        results.sort_by(|a, b| {
1161            let pa = a.module_path.to_string_lossy();
1162            let pb = b.module_path.to_string_lossy();
1163            match pa.cmp(&pb) {
1164                std::cmp::Ordering::Equal => a.address.cmp(&b.address),
1165                other => other,
1166            }
1167        });
1168        results
1169    }
1170
1171    /// Query source-line debug information across all modules.
1172    pub fn query_source_line(
1173        &self,
1174        file_path: &str,
1175        line_number: u32,
1176    ) -> Result<Vec<AddressQueryResult>> {
1177        let module_addresses = self.lookup_addresses_by_source_line(file_path, line_number);
1178        self.query_module_addresses_for_source_line(module_addresses, file_path, line_number)
1179    }
1180
1181    /// Query source-line debug information across all modules, skipping
1182    /// addresses that fail to resolve so callers can still display partial
1183    /// results.
1184    pub fn query_source_line_best_effort(
1185        &self,
1186        file_path: &str,
1187        line_number: u32,
1188    ) -> Result<Vec<AddressQueryResult>> {
1189        let module_addresses = self.lookup_addresses_by_source_line(file_path, line_number);
1190        self.query_module_addresses_for_source_line_best_effort(
1191            module_addresses,
1192            file_path,
1193            line_number,
1194            &format!("source line '{file_path}:{line_number}'"),
1195        )
1196    }
1197
1198    /// Query a specific address within a module.
1199    pub fn query_address<P: AsRef<Path>>(
1200        &self,
1201        module_path: P,
1202        address: u64,
1203    ) -> Result<AddressQueryResult> {
1204        let module_address = ModuleAddress::new(module_path.as_ref().to_path_buf(), address);
1205        self.build_address_query_result(&module_address)
1206    }
1207
1208    /// Get all function names (cross-module)
1209    pub fn get_all_function_names(&self) -> Vec<String> {
1210        let mut all_names = std::collections::HashSet::new();
1211        for module_data in self.modules.values() {
1212            for name in module_data.get_function_names() {
1213                all_names.insert(name.clone());
1214            }
1215        }
1216        all_names.into_iter().collect()
1217    }
1218
1219    /// Get statistics for debugging
1220    pub fn get_stats(&self) -> AnalyzerStats {
1221        let mut total_functions = 0;
1222        let mut total_variables = 0;
1223        let mut total_line_headers = 0;
1224
1225        for module_data in self.modules.values() {
1226            total_functions += module_data.get_function_names().len();
1227            total_variables += module_data.get_variable_names().len();
1228            total_line_headers += module_data.get_line_header_count();
1229        }
1230
1231        AnalyzerStats {
1232            pid: self.pid,
1233            module_count: self.modules.len(),
1234            total_functions,
1235            total_variables,
1236            total_line_headers,
1237        }
1238    }
1239
1240    /// Get module statistics (compatible with ghostscope-binary's ModuleStats)
1241    pub fn get_module_stats(&self) -> ModuleStats {
1242        let mut total_symbols = 0;
1243        let mut executable_modules = 0;
1244        let mut library_modules = 0;
1245        let mut modules_with_debug_info = 0;
1246
1247        for (module_path, module_data) in &self.modules {
1248            let function_names = module_data.get_function_names();
1249            total_symbols += function_names.len();
1250            if !matches!(
1251                module_data.get_debug_info_source(),
1252                DebugInfoSource::Missing
1253            ) {
1254                modules_with_debug_info += 1;
1255            }
1256
1257            // Check if module is executable (main binary) or library
1258            if self.is_main_executable_module(module_path) {
1259                executable_modules += 1;
1260            } else {
1261                library_modules += 1;
1262            }
1263        }
1264
1265        ModuleStats {
1266            total_modules: self.modules.len(),
1267            executable_modules,
1268            library_modules,
1269            total_symbols,
1270            modules_with_debug_info,
1271        }
1272    }
1273
1274    /// Get main executable module information
1275    pub fn get_main_executable(&self) -> Option<MainExecutableInfo> {
1276        // Find the main executable module (usually the first non-library module)
1277        for module_path in self.modules.keys() {
1278            if self.is_main_executable_module(module_path) {
1279                return Some(MainExecutableInfo {
1280                    path: module_path.to_string_lossy().to_string(),
1281                });
1282            }
1283        }
1284        None
1285    }
1286
1287    /// Check if a module is the main executable (not a shared library)
1288    fn is_main_executable_module(&self, module_path: &Path) -> bool {
1289        // Heuristic: main executable usually doesn't have .so extension and contains the process name
1290        let filename = module_path
1291            .file_name()
1292            .and_then(|name| name.to_str())
1293            .unwrap_or("");
1294
1295        // Not a shared library
1296        !filename.contains(".so") &&
1297        // Not a system library path
1298        !module_path.to_string_lossy().starts_with("/lib") &&
1299        !module_path.to_string_lossy().starts_with("/usr/lib")
1300    }
1301
1302    /// Get list of all function names across all modules
1303    pub fn list_functions(&self) -> Vec<String> {
1304        let mut all_functions = Vec::new();
1305
1306        for module_data in self.modules.values() {
1307            let function_names = module_data.get_function_names();
1308            for name in function_names {
1309                all_functions.push(name.clone());
1310            }
1311        }
1312
1313        // Remove duplicates and sort
1314        all_functions.sort();
1315        all_functions.dedup();
1316
1317        tracing::debug!(
1318            "Listed {} unique functions across {} modules",
1319            all_functions.len(),
1320            self.modules.len()
1321        );
1322
1323        all_functions
1324    }
1325
1326    /// Lookup functions by pattern (simplified - exact match only for now)
1327    pub fn lookup_functions_by_pattern(&self, pattern: &str) -> Vec<String> {
1328        let all_functions = self.list_functions();
1329        all_functions
1330            .into_iter()
1331            .filter(|name| name.contains(pattern))
1332            .collect()
1333    }
1334
1335    /// Get all function names (alias for compatibility)
1336    pub fn lookup_all_function_names(&self) -> Vec<String> {
1337        self.list_functions()
1338    }
1339
1340    /// Get PID (accessor for private field)
1341    pub fn get_pid(&self) -> u32 {
1342        self.pid
1343    }
1344
1345    /// Get shared library information (compatibility method)
1346    pub fn get_shared_library_info(&self) -> Vec<SharedLibraryInfo> {
1347        self.modules
1348            .iter()
1349            .filter(|(path, _)| self.is_shared_library(path))
1350            .map(|(path, module_data)| {
1351                let mapping = module_data.module_mapping();
1352                let debug_file_path = module_data
1353                    .get_debug_file_path()
1354                    .map(|p| p.to_string_lossy().to_string());
1355
1356                SharedLibraryInfo {
1357                    from_address: mapping.loaded_address.unwrap_or(0),
1358                    to_address: mapping.loaded_address.map_or(0, |addr| addr + mapping.size),
1359                    symbols_read: !module_data.get_function_names().is_empty(),
1360                    // Reflect actual DWARF availability (embedded or via .gnu_debuglink)
1361                    debug_info_available: module_data.has_dwarf_info(),
1362                    library_path: path.to_string_lossy().to_string(),
1363                    size: mapping.size,
1364                    debug_file_path,
1365                }
1366            })
1367            .collect()
1368    }
1369
1370    /// Get executable file information (for "info file" command)
1371    pub fn get_executable_file_info(&self) -> Option<ExecutableFileInfo> {
1372        // Find the primary executable (not a shared library)
1373        let executable = self
1374            .modules
1375            .iter()
1376            .find(|(path, _)| !self.is_shared_library(path))?;
1377
1378        let (exe_path, module_data) = executable;
1379        let file_path = exe_path.to_string_lossy().to_string();
1380
1381        // Parse the ELF file to get detailed information
1382        let file_bytes = std::fs::read(exe_path).ok()?;
1383        let obj = object::File::parse(&file_bytes[..]).ok()?;
1384
1385        // Get file type
1386        let file_type = match obj.format() {
1387            object::BinaryFormat::Elf => {
1388                if obj.is_64() {
1389                    "ELF 64-bit executable"
1390                } else {
1391                    "ELF 32-bit executable"
1392                }
1393            }
1394            _ => "Unknown format",
1395        }
1396        .to_string();
1397
1398        // Check if has symbols
1399        let has_symbols = !module_data.get_function_names().is_empty()
1400            || obj.symbols().count() > 0
1401            || obj.dynamic_symbols().count() > 0;
1402
1403        // Check if has debug info - check if DWARF was successfully loaded
1404        // This includes both embedded DWARF and debug link external files
1405        let has_debug_info = module_data.has_dwarf_info();
1406
1407        // Get debug file path if using separate debug file (e.g., via .gnu_debuglink)
1408        let debug_file_path = module_data.get_debug_file_path();
1409
1410        // Load bias for PID mode from module mapping (if available)
1411        let load_bias = if self.pid != 0 {
1412            module_data.module_mapping().loaded_address.unwrap_or(0)
1413        } else {
1414            0
1415        };
1416
1417        // Get entry point (add load bias in PID mode)
1418        let entry_point = Some(obj.entry() + load_bias);
1419
1420        // Get .text section info (add load bias in PID mode)
1421        let text_section = obj.section_by_name(".text").map(|section| {
1422            let addr = section.address() + load_bias;
1423            let size = section.size();
1424            SectionInfo {
1425                start_address: addr,
1426                end_address: addr + size,
1427                size,
1428            }
1429        });
1430
1431        // Get .data section info (add load bias in PID mode)
1432        let data_section = obj.section_by_name(".data").map(|section| {
1433            let addr = section.address() + load_bias;
1434            let size = section.size();
1435            SectionInfo {
1436                start_address: addr,
1437                end_address: addr + size,
1438                size,
1439            }
1440        });
1441
1442        // Determine mode description based on pid
1443        let mode_description = if self.pid != 0 {
1444            format!("Attached to process {} (PID mode)", self.pid)
1445        } else {
1446            "Static analysis mode (target file specified with -t)".to_string()
1447        };
1448
1449        Some(ExecutableFileInfo {
1450            file_path,
1451            file_type,
1452            entry_point,
1453            has_symbols,
1454            has_debug_info,
1455            debug_file_path: debug_file_path.map(|p| p.to_string_lossy().to_string()),
1456            text_section,
1457            data_section,
1458            mode_description,
1459        })
1460    }
1461
1462    // NOTE: Runtime section offsets are handled by ghostscope-coordinator.
1463
1464    /// Check if a module is a shared library
1465    fn is_shared_library(&self, module_path: &Path) -> bool {
1466        let filename = module_path
1467            .file_name()
1468            .and_then(|name| name.to_str())
1469            .unwrap_or("");
1470
1471        // Shared libraries typically have .so extension or contain .so
1472        filename.contains(".so")
1473            || module_path.to_string_lossy().starts_with("/lib")
1474            || module_path.to_string_lossy().starts_with("/usr/lib")
1475    }
1476
1477    /// Get grouped file info by module (compatibility method)
1478    pub fn get_grouped_file_info_by_module(&self) -> Result<Vec<(String, Vec<SimpleFileInfo>)>> {
1479        let mut grouped = Vec::new();
1480
1481        for (module_path, module_data) in &self.modules {
1482            let files = module_data.get_all_files();
1483            if !files.is_empty() {
1484                let simple_files: Vec<SimpleFileInfo> = files
1485                    .into_iter()
1486                    .map(|source_file| SimpleFileInfo {
1487                        full_path: source_file.full_path,
1488                        basename: source_file.filename,
1489                        directory: source_file.directory_path,
1490                    })
1491                    .collect();
1492
1493                grouped.push((module_path.to_string_lossy().to_string(), simple_files));
1494            }
1495        }
1496
1497        Ok(grouped)
1498    }
1499}
1500
1501///
1502/// Module statistics compatible with ghostscope-binary
1503#[derive(Debug, Clone)]
1504pub struct ModuleStats {
1505    pub total_modules: usize,
1506    pub executable_modules: usize,
1507    pub library_modules: usize,
1508    pub total_symbols: usize,
1509    pub modules_with_debug_info: usize,
1510}
1511
1512/// Main executable information
1513#[derive(Debug, Clone)]
1514pub struct MainExecutableInfo {
1515    pub path: String,
1516}
1517
1518/// Statistics for debugging and monitoring
1519#[derive(Debug, Clone)]
1520pub struct AnalyzerStats {
1521    pub pid: u32,
1522    pub module_count: usize,
1523    pub total_functions: usize,
1524    pub total_variables: usize,
1525    pub total_line_headers: usize,
1526}
1527
1528/// Shared library information (compatible with ghostscope-ui)
1529#[derive(Debug, Clone)]
1530pub struct SharedLibraryInfo {
1531    pub from_address: u64,               // Starting address in memory
1532    pub to_address: u64,                 // Ending address in memory
1533    pub symbols_read: bool,              // Whether symbols were successfully read
1534    pub debug_info_available: bool,      // Whether debug information is available
1535    pub library_path: String,            // Full path to the library file
1536    pub size: u64,                       // Size of the library in memory
1537    pub debug_file_path: Option<String>, // Path to separate debug file (if via .gnu_debuglink)
1538}
1539
1540/// Executable file information (for "info file" command)
1541#[derive(Debug, Clone)]
1542pub struct ExecutableFileInfo {
1543    pub file_path: String,
1544    pub file_type: String,
1545    pub entry_point: Option<u64>,
1546    pub has_symbols: bool,
1547    pub has_debug_info: bool,
1548    pub debug_file_path: Option<String>,
1549    pub text_section: Option<SectionInfo>,
1550    pub data_section: Option<SectionInfo>,
1551    pub mode_description: String,
1552}
1553
1554/// Section information for executable files
1555#[derive(Debug, Clone)]
1556pub struct SectionInfo {
1557    pub start_address: u64,
1558    pub end_address: u64,
1559    pub size: u64,
1560}
1561
1562/// Simple file information compatible with ghostscope-binary
1563#[derive(Debug, Clone)]
1564pub struct SimpleFileInfo {
1565    pub full_path: String,
1566    pub basename: String,
1567    pub directory: String,
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572    use super::*;
1573
1574    fn global_plan(name: &str, address: u64) -> VariableReadPlan {
1575        VariableReadPlan {
1576            name: name.to_string(),
1577            type_name: "int".to_string(),
1578            access_path: crate::VariableAccessPath::default(),
1579            module_path: None,
1580            dwarf_type: Some(crate::TypeInfo::BaseType {
1581                name: "int".to_string(),
1582                size: 4,
1583                encoding: gimli::constants::DW_ATE_signed.0 as u16,
1584            }),
1585            declaration: None,
1586            type_id: None,
1587            location: VariableLocation::Address(AddressExpr::constant(address)),
1588            availability: Availability::Available,
1589            scope_depth: 0,
1590            is_parameter: false,
1591            is_artificial: false,
1592            pc_range: None,
1593            inline_context: None,
1594            provenance: Provenance::Synthesized {
1595                detail: "test".to_string(),
1596            },
1597        }
1598    }
1599
1600    fn visible_var(name: &str, scope_depth: usize) -> VisibleVariable {
1601        VisibleVariable {
1602            name: name.to_string(),
1603            type_name: "int".to_string(),
1604            dwarf_type: Some(crate::TypeInfo::BaseType {
1605                name: "int".to_string(),
1606                size: 4,
1607                encoding: gimli::constants::DW_ATE_signed.0 as u16,
1608            }),
1609            declaration: None,
1610            type_id: None,
1611            location: VariableLocation::RegisterValue { dwarf_reg: 0 },
1612            availability: Availability::Available,
1613            scope_depth,
1614            is_parameter: false,
1615            is_artificial: false,
1616        }
1617    }
1618
1619    fn diagnostic(
1620        name: &str,
1621        scope_depth: usize,
1622        detail: &str,
1623    ) -> crate::semantics::VariableQueryDiagnostic {
1624        crate::semantics::VariableQueryDiagnostic {
1625            pc: 0x1234,
1626            name: Some(name.to_string()),
1627            scope_depth,
1628            availability: Availability::Unsupported(crate::UnsupportedReason::ExpressionShape {
1629                detail: detail.to_string(),
1630            }),
1631            detail: detail.to_string(),
1632        }
1633    }
1634
1635    #[test]
1636    fn variable_selection_rejects_inner_diagnostic_over_outer_match() {
1637        let err = DwarfAnalyzer::select_visible_variable_by_name(
1638            0x1234,
1639            "state",
1640            vec![visible_var("state", 1)],
1641            &[diagnostic("state", 2, "DW_OP_bad is unsupported")],
1642        )
1643        .expect_err("inner unavailable variable should block outer fallback");
1644
1645        assert!(err.to_string().contains("Unavailable variable 'state'"));
1646        assert!(err.to_string().contains("DW_OP_bad is unsupported"));
1647    }
1648
1649    #[test]
1650    fn variable_selection_keeps_inner_match_over_outer_diagnostic() {
1651        let selected = DwarfAnalyzer::select_visible_variable_by_name(
1652            0x1234,
1653            "state",
1654            vec![visible_var("state", 2)],
1655            &[diagnostic("state", 1, "outer variable is unavailable")],
1656        )
1657        .expect("outer diagnostic should not block inner match")
1658        .expect("inner match should be returned");
1659
1660        assert_eq!(selected.name, "state");
1661        assert_eq!(selected.scope_depth, 2);
1662    }
1663
1664    #[test]
1665    fn global_plan_selection_rejects_ambiguous_matches() {
1666        let err = DwarfAnalyzer::select_unambiguous_global_plan(
1667            "state",
1668            vec![
1669                (PathBuf::from("/tmp/a"), global_plan("state", 0x1000)),
1670                (PathBuf::from("/tmp/b"), global_plan("state", 0x2000)),
1671            ],
1672        )
1673        .expect_err("multiple global candidates should be ambiguous");
1674
1675        assert!(err.to_string().contains("Ambiguous global 'state'"));
1676        assert!(err.to_string().contains("2 matches"));
1677    }
1678
1679    #[test]
1680    fn global_plan_selection_accepts_single_match() {
1681        let selected = DwarfAnalyzer::select_unambiguous_global_plan(
1682            "state",
1683            vec![(PathBuf::from("/tmp/a"), global_plan("state", 0x1000))],
1684        )
1685        .expect("single global candidate should be accepted")
1686        .expect("single global candidate should be returned");
1687
1688        assert_eq!(selected.0, PathBuf::from("/tmp/a"));
1689        assert_eq!(selected.1.name, "state");
1690    }
1691
1692    #[test]
1693    fn global_plan_selection_prefers_current_module_match() {
1694        let selected = DwarfAnalyzer::select_global_plan_with_preferred_module(
1695            "state",
1696            Path::new("/tmp/current"),
1697            vec![
1698                (PathBuf::from("/tmp/other"), global_plan("state", 0x2000)),
1699                (PathBuf::from("/tmp/current"), global_plan("state", 0x1000)),
1700            ],
1701        )
1702        .expect("current module candidate should be accepted")
1703        .expect("current module candidate should be returned");
1704
1705        assert_eq!(selected.0, PathBuf::from("/tmp/current"));
1706        assert_eq!(
1707            selected.1.location,
1708            VariableLocation::Address(AddressExpr::constant(0x1000))
1709        );
1710    }
1711
1712    #[test]
1713    fn global_plan_selection_rejects_ambiguous_current_module_matches() {
1714        let err = DwarfAnalyzer::select_global_plan_with_preferred_module(
1715            "state",
1716            Path::new("/tmp/current"),
1717            vec![
1718                (PathBuf::from("/tmp/current"), global_plan("state", 0x1000)),
1719                (PathBuf::from("/tmp/current"), global_plan("state", 0x1004)),
1720                (PathBuf::from("/tmp/other"), global_plan("state", 0x2000)),
1721            ],
1722        )
1723        .expect_err("duplicate current-module candidates should be ambiguous");
1724
1725        assert!(err.to_string().contains("Ambiguous global 'state'"));
1726        assert!(err.to_string().contains("2 matches"));
1727        assert!(err.to_string().contains("/tmp/current"));
1728        assert!(!err.to_string().contains("/tmp/other"));
1729    }
1730}