Skip to main content

ghostscope_dwarf/analyzer/
plan_pc.rs

1use super::DwarfAnalyzer;
2use crate::{
3    core::{ModuleAddress, Provenance, Result},
4    semantics::{
5        AddressSpaceInfo, FunctionParameter, PcContext, PcLineInfo, PcRange, PlanError,
6        VariableAccessPath, VariableAccessSegment, VariableReadPlan, VisibleVariable,
7        VisibleVariablesResult,
8    },
9};
10use std::path::Path;
11
12impl DwarfAnalyzer {
13    /// Resolve a module-address pair into the first PC-centered semantic context.
14    ///
15    /// Today `ModuleAddress.address` is the module/DWARF PC used by the existing
16    /// query APIs, so `pc` and `normalized_pc` intentionally match. Runtime
17    /// rebasing details are preserved in `address_space` for future lowering.
18    pub fn resolve_pc(&self, module_address: &ModuleAddress) -> Result<PcContext> {
19        if let Some(context) = self
20            .pc_context_cache
21            .read()
22            .expect("PC context cache lock poisoned")
23            .get(&module_address.module_path, module_address.address)
24        {
25            return Ok(context);
26        }
27
28        let context = self.resolve_pc_uncached(module_address)?;
29        self.pc_context_cache
30            .write()
31            .expect("PC context cache lock poisoned")
32            .insert(
33                module_address.module_path.clone(),
34                module_address.address,
35                context.clone(),
36            );
37        Ok(context)
38    }
39
40    fn resolve_pc_uncached(&self, module_address: &ModuleAddress) -> Result<PcContext> {
41        let module_path = self
42            .loaded_module_path_for(&module_address.module_path)
43            .ok_or_else(|| {
44                anyhow::anyhow!("Module {} not loaded", module_address.module_display())
45            })?;
46        let module_data = self.modules.get(module_path).ok_or_else(|| {
47            anyhow::anyhow!("Module {} not loaded", module_address.module_display())
48        })?;
49        let module = self.module_id_for_path(module_path).ok_or_else(|| {
50            anyhow::anyhow!(
51                "Module {} has no semantic module id",
52                module_address.module_display()
53            )
54        })?;
55
56        let (cu, function, lexical_scopes, inline_chain) = module_data
57            .resolve_pc_scopes(module, module_address.address)
58            .unwrap_or_else(|error| {
59                tracing::debug!(
60                    "Failed to resolve semantic PC scopes for {}:0x{:x}: {}",
61                    module_address.module_display(),
62                    module_address.address,
63                    error
64                );
65                (None, None, Vec::new(), Vec::new())
66            });
67        let source_location = module_data.lookup_source_location(module_address.address);
68        let line = source_location.map(|location| PcLineInfo {
69            file_path: location.file_path,
70            line_number: location.line_number,
71            column: location.column,
72            address: location.address,
73        });
74        let function_name = module_data.find_function_name_by_address(module_address.address);
75        let is_inline = module_data.is_inline_at(module_address.address);
76        let mapping = module_data.module_mapping();
77
78        Ok(PcContext {
79            module,
80            pc: module_address.address,
81            normalized_pc: module_address.address,
82            cu,
83            function,
84            function_name,
85            lexical_scopes,
86            inline_chain,
87            is_inline,
88            line,
89            address_space: AddressSpaceInfo {
90                module_path: Some(mapping.path.clone()),
91                runtime_base: mapping.loaded_address,
92                link_base: None,
93            },
94        })
95    }
96
97    /// Return variables visible at a previously resolved PC context.
98    pub fn visible_variables(&self, ctx: &PcContext) -> Result<Vec<VisibleVariable>> {
99        Ok(self.visible_variables_with_diagnostics(ctx)?.variables)
100    }
101
102    /// Return only function formal parameters for display.
103    ///
104    /// This does not evaluate locations, compute frame-base/CFA, or resolve
105    /// visible lexical variables. It is intended for inexpensive function
106    /// signature rendering in hot paths such as backtrace output.
107    pub fn function_parameters(&self, ctx: &PcContext) -> Result<Vec<FunctionParameter>> {
108        let Some(function) = ctx.function else {
109            return Ok(Vec::new());
110        };
111        let module_address = self.module_address_for_context(ctx)?;
112
113        self.modules
114            .get(&module_address.module_path)
115            .ok_or_else(|| {
116                anyhow::anyhow!("Module {} not loaded", module_address.module_display())
117            })?
118            .function_parameters(function)
119    }
120
121    /// Return variables visible at a PC context plus non-fatal DWARF diagnostics.
122    pub fn visible_variables_with_diagnostics(
123        &self,
124        ctx: &PcContext,
125    ) -> Result<VisibleVariablesResult> {
126        let module_address = self.module_address_for_context(ctx)?;
127
128        let (variables, diagnostics) = self
129            .modules
130            .get(&module_address.module_path)
131            .ok_or_else(|| {
132                anyhow::anyhow!("Module {} not loaded", module_address.module_display())
133            })?
134            .get_visible_variables_at_address_best_effort_with_diagnostics(
135                ctx.module,
136                module_address.address,
137            )?;
138        let mut variables: Vec<VisibleVariable> = variables
139            .into_iter()
140            .map(|variable| variable.visible_variable())
141            .collect();
142
143        variables.sort_by(|a, b| {
144            a.scope_depth
145                .cmp(&b.scope_depth)
146                .then_with(|| b.is_parameter.cmp(&a.is_parameter))
147                .then_with(|| a.name.cmp(&b.name))
148        });
149        Ok(VisibleVariablesResult {
150            variables,
151            diagnostics,
152        })
153    }
154
155    pub(super) fn module_address_for_context(&self, ctx: &PcContext) -> Result<ModuleAddress> {
156        let module_path = match ctx.address_space.module_path.as_deref() {
157            Some(path) => path,
158            None => self.module_path_for_id(ctx.module).ok_or_else(|| {
159                anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module)
160            })?,
161        };
162        Ok(ModuleAddress::new(
163            module_path.to_path_buf(),
164            ctx.normalized_pc,
165        ))
166    }
167
168    pub(super) fn is_value_backed_aggregate_access_error(err: &anyhow::Error) -> bool {
169        err.downcast_ref::<PlanError>()
170            .is_some_and(PlanError::is_value_backed_aggregate_access)
171    }
172
173    pub(super) fn read_plan_from_variable(
174        variable: crate::parser::ParsedVariable,
175        provenance: Provenance,
176    ) -> VariableReadPlan {
177        VariableReadPlan::from_visible_variable(variable.visible_variable(), provenance)
178    }
179
180    fn attach_pc_context(ctx: &PcContext, mut plan: VariableReadPlan) -> VariableReadPlan {
181        plan.pc_range = Some(PcRange {
182            start: ctx.normalized_pc,
183            end: ctx.normalized_pc,
184        });
185        plan.inline_context = ctx.inline_chain.last().and_then(|frame| frame.context);
186        plan.module_path = ctx.address_space.module_path.clone();
187        plan
188    }
189
190    pub(super) fn plan_access_path_with_type_completion(
191        &self,
192        module_path: &Path,
193        mut plan: VariableReadPlan,
194        path: &VariableAccessPath,
195    ) -> Result<VariableReadPlan> {
196        for segment in &path.segments {
197            let pointer_type_name = plan.type_name.clone();
198            self.complete_unknown_pointer_target_type(module_path, &mut plan, &pointer_type_name);
199            plan = plan.plan_access_path(&VariableAccessPath::new(vec![segment.clone()]))?;
200            if matches!(segment, VariableAccessSegment::Dereference) {
201                self.complete_unknown_pointer_target_type(
202                    module_path,
203                    &mut plan,
204                    &pointer_type_name,
205                );
206            }
207        }
208
209        Ok(plan)
210    }
211
212    /// Plan a visible variable by source name at a previously resolved PC context.
213    ///
214    /// Exact names are preferred over producer-synthesized names like `name@...`.
215    pub fn plan_variable_by_name(
216        &self,
217        ctx: &PcContext,
218        name: &str,
219    ) -> Result<Option<VariableReadPlan>> {
220        let VisibleVariablesResult {
221            variables: visible_variables,
222            diagnostics,
223        } = self.visible_variables_with_diagnostics(ctx)?;
224
225        Self::select_visible_variable_by_name(
226            ctx.normalized_pc,
227            name,
228            visible_variables,
229            &diagnostics,
230        )
231        .map(|variable| {
232            variable.map(|variable| {
233                Self::attach_pc_context(
234                    ctx,
235                    VariableReadPlan::from_visible_variable(variable, Provenance::DirectDie),
236                )
237            })
238        })
239    }
240
241    pub(super) fn select_visible_variable_by_name(
242        pc: u64,
243        name: &str,
244        visible_variables: Vec<VisibleVariable>,
245        diagnostics: &[crate::semantics::VariableQueryDiagnostic],
246    ) -> Result<Option<VisibleVariable>> {
247        let synthesized_prefix = format!("{name}@");
248        let matching_diagnostics = diagnostics
249            .iter()
250            .filter(|diagnostic| {
251                diagnostic.name.as_deref().is_some_and(|diagnostic_name| {
252                    diagnostic_name == name || diagnostic_name.starts_with(&synthesized_prefix)
253                })
254            })
255            .collect::<Vec<_>>();
256
257        let exact_matches = visible_variables
258            .iter()
259            .filter(|variable| variable.name == name)
260            .cloned()
261            .collect::<Vec<_>>();
262
263        let mut candidates = if exact_matches.is_empty() {
264            visible_variables
265                .into_iter()
266                .filter(|variable| variable.name.starts_with(&synthesized_prefix))
267                .collect::<Vec<_>>()
268        } else {
269            exact_matches
270        };
271
272        if candidates.is_empty() {
273            if let Some(diagnostic) = matching_diagnostics
274                .iter()
275                .max_by_key(|diagnostic| diagnostic.scope_depth)
276            {
277                return Err(anyhow::anyhow!(
278                    "Unavailable variable '{name}' at PC 0x{:x}: {}",
279                    pc,
280                    diagnostic.detail
281                ));
282            }
283            return Ok(None);
284        }
285
286        let max_scope_depth = candidates
287            .iter()
288            .map(|variable| variable.scope_depth)
289            .max()
290            .unwrap_or(0);
291        if let Some(diagnostic) = matching_diagnostics
292            .iter()
293            .filter(|diagnostic| diagnostic.scope_depth > max_scope_depth)
294            .max_by_key(|diagnostic| diagnostic.scope_depth)
295        {
296            return Err(anyhow::anyhow!(
297                "Unavailable variable '{name}' at PC 0x{:x}: {}",
298                pc,
299                diagnostic.detail
300            ));
301        }
302        candidates.retain(|variable| variable.scope_depth == max_scope_depth);
303
304        if candidates.iter().any(|variable| !variable.is_artificial) {
305            candidates.retain(|variable| !variable.is_artificial);
306        }
307
308        candidates.dedup();
309        if candidates.len() > 1 {
310            let names = candidates
311                .iter()
312                .map(|variable| variable.name.as_str())
313                .collect::<Vec<_>>()
314                .join(", ");
315            return Err(anyhow::anyhow!(
316                "Ambiguous variable '{name}' at PC 0x{:x}: candidates [{}]",
317                pc,
318                names
319            ));
320        }
321
322        Ok(candidates.into_iter().next())
323    }
324
325    /// Plan a visible variable by stable variable id at a previously resolved PC context.
326    ///
327    /// This is the identity-based path for callers that first enumerate
328    /// `visible_variables(ctx)` and then request a read plan without relying on
329    /// a potentially ambiguous source name.
330    pub fn plan_variable(
331        &self,
332        ctx: &PcContext,
333        variable_id: crate::VariableId,
334    ) -> Result<Option<VariableReadPlan>> {
335        if variable_id.declaration.module != ctx.module {
336            return Err(anyhow::anyhow!(
337                "VariableId module {:?} does not match PcContext module {:?}",
338                variable_id.declaration.module,
339                ctx.module
340            ));
341        }
342
343        let matches = self
344            .visible_variables(ctx)?
345            .into_iter()
346            .filter(|variable| variable.declaration == Some(variable_id.declaration))
347            .collect::<Vec<_>>();
348
349        match matches.as_slice() {
350            [] => Ok(None),
351            [variable] => Ok(Some(Self::attach_pc_context(
352                ctx,
353                VariableReadPlan::from_visible_variable(variable.clone(), Provenance::DirectDie),
354            ))),
355            _ => Err(anyhow::anyhow!(
356                "Ambiguous VariableId {:?} at PC 0x{:x}: {} visible matches",
357                variable_id,
358                ctx.normalized_pc,
359                matches.len()
360            )),
361        }
362    }
363
364    /// Plan a source-level access path from a visible variable id at a PC context.
365    pub fn plan_variable_access(
366        &self,
367        ctx: &PcContext,
368        variable_id: crate::VariableId,
369        path: &VariableAccessPath,
370    ) -> Result<Option<VariableReadPlan>> {
371        let Some(plan) = self.plan_variable(ctx, variable_id)? else {
372            return Ok(None);
373        };
374        let module_path = self
375            .module_path_for_id(ctx.module)
376            .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?
377            .to_path_buf();
378
379        self.plan_access_path_with_type_completion(&module_path, plan, path)
380            .map(Some)
381    }
382
383    /// Plan a source-level access path from a visible variable at a PC context.
384    pub fn plan_variable_access_by_name(
385        &self,
386        ctx: &PcContext,
387        name: &str,
388        path: &VariableAccessPath,
389    ) -> Result<Option<VariableReadPlan>> {
390        let Some(plan) = self.plan_variable_by_name(ctx, name)? else {
391            return Ok(None);
392        };
393        let module_path = self
394            .module_path_for_id(ctx.module)
395            .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?
396            .to_path_buf();
397
398        self.plan_access_path_with_type_completion(&module_path, plan, path)
399            .map(Some)
400    }
401
402    /// Return variables visible at a module address as semantic views.
403    ///
404    /// # Arguments
405    /// * `module_address` - Module address containing both module path and address offset
406    pub(super) fn visible_variables_at_address(
407        &self,
408        module_address: &ModuleAddress,
409    ) -> Result<Vec<VisibleVariable>> {
410        tracing::info!(
411            "Looking up variables at address 0x{:x} in module {}",
412            module_address.address,
413            module_address.module_display()
414        );
415        let ctx = self.resolve_pc(module_address)?;
416        self.visible_variables(&ctx)
417    }
418}