Skip to main content

ghostscope_dwarf/semantics/
variable_plan.rs

1//! Variable semantic plans before runtime-specific lowering.
2
3use crate::core::{
4    AddressExpr, Availability, DieRef, HelperMode, InlineContextId, MemoryAccessSize,
5    PieceLocation, PlanExprOp, Provenance, Result, RuntimeCapabilities, RuntimeRequirement, TypeId,
6    UnsupportedReason, VariableId, VariableLocation, VerifierRisk,
7};
8use crate::semantics::{
9    indexable_element_layout, member_layout, strip_type_aliases, PcRange, TypeLayoutError,
10};
11use crate::TypeInfo;
12use std::path::PathBuf;
13
14/// Owned semantic view returned by PC-context variable queries.
15#[derive(Debug, Clone, PartialEq)]
16pub struct VisibleVariable {
17    pub name: String,
18    pub type_name: String,
19    pub dwarf_type: Option<TypeInfo>,
20    pub declaration: Option<DieRef>,
21    pub type_id: Option<TypeId>,
22    pub location: VariableLocation,
23    pub availability: Availability,
24    pub scope_depth: usize,
25    pub is_parameter: bool,
26    pub is_artificial: bool,
27}
28
29/// Diagnostic produced while answering a PC-sensitive variable query.
30#[derive(Debug, Clone, PartialEq)]
31pub struct VariableQueryDiagnostic {
32    pub pc: u64,
33    pub name: Option<String>,
34    pub scope_depth: usize,
35    pub availability: Availability,
36    pub detail: String,
37}
38
39/// Visible variables plus non-fatal diagnostics from best-effort discovery.
40#[derive(Debug, Clone, PartialEq)]
41pub struct VisibleVariablesResult {
42    pub variables: Vec<VisibleVariable>,
43    pub diagnostics: Vec<VariableQueryDiagnostic>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum VariableLoweringKind {
48    DirectValue,
49    UserMemoryRead,
50    Composite,
51    Unavailable,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct VariableLoweringPlan {
56    pub kind: VariableLoweringKind,
57    pub availability: Availability,
58    pub requirements: Vec<RuntimeRequirement>,
59    pub helper_mode: HelperMode,
60    pub required_registers: Vec<u16>,
61    pub estimated_stack_bytes: usize,
62    pub verifier_risk: VerifierRisk,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum AddressOrigin {
67    LinkTime,
68    LinkTimeBase,
69    RuntimeDerived,
70    Unknown,
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub struct PlannedAddress {
75    pub kind: PlannedAddressKind,
76    pub origin: AddressOrigin,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum RuntimeComputedKind {
81    Address,
82    Value,
83}
84
85/// Runtime expression selected by DWARF semantic planning.
86///
87/// This is intentionally still expressive enough to carry the DWARF evaluator's
88/// stack program, but it is no longer a bare location/value result. The planner
89/// classifies the expression as an address or a value before compiler lowering.
90#[derive(Debug, Clone, PartialEq)]
91pub struct RuntimeComputedExpr {
92    kind: RuntimeComputedKind,
93    ops: Vec<PlanExprOp>,
94}
95
96impl RuntimeComputedExpr {
97    pub(crate) fn address(ops: Vec<PlanExprOp>) -> Self {
98        Self {
99            kind: RuntimeComputedKind::Address,
100            ops,
101        }
102    }
103
104    pub(crate) fn value(ops: Vec<PlanExprOp>) -> Self {
105        Self {
106            kind: RuntimeComputedKind::Value,
107            ops,
108        }
109    }
110
111    pub fn ops(&self) -> &[PlanExprOp] {
112        &self.ops
113    }
114
115    pub fn kind(&self) -> RuntimeComputedKind {
116        self.kind
117    }
118
119    pub fn runtime_requirements(&self) -> Vec<RuntimeRequirement> {
120        requirements_for_steps(&self.ops)
121    }
122
123    pub fn required_registers(&self) -> Vec<u16> {
124        registers_for_steps(&self.ops)
125    }
126
127    pub fn estimated_stack_bytes(&self) -> usize {
128        estimate_steps_stack_bytes(&self.ops)
129    }
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub enum PlannedAddressKind {
134    Constant { address: u64 },
135    RegisterOffset { dwarf_reg: u16, offset: i64 },
136    FrameBaseRelative { offset: i64 },
137    RuntimeComputed { expr: RuntimeComputedExpr },
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub enum PlannedValue {
142    Constant {
143        value: i64,
144        size: MemoryAccessSize,
145    },
146    RegisterValue {
147        dwarf_reg: u16,
148        size: MemoryAccessSize,
149    },
150    RuntimeComputed {
151        expr: RuntimeComputedExpr,
152        result_size: MemoryAccessSize,
153    },
154    ImplicitBytes(Vec<u8>),
155    AddressValue {
156        address: PlannedAddress,
157        size: MemoryAccessSize,
158    },
159}
160
161#[derive(Debug, Clone, PartialEq)]
162pub enum VariableMaterialization {
163    DirectValue { value: PlannedValue },
164    UserMemoryRead { address: PlannedAddress },
165    Composite { pieces: Vec<PieceLocation> },
166    Unavailable { availability: Availability },
167}
168
169#[derive(Debug, Clone, PartialEq)]
170pub enum LvalueAddressPlan {
171    Address { address: PlannedAddress },
172    Unavailable { availability: Availability },
173}
174
175#[derive(Debug, Clone, PartialEq)]
176pub struct VariableMaterializationPlan {
177    pub name: String,
178    pub type_name: String,
179    pub access_path: VariableAccessPath,
180    pub module_path: Option<PathBuf>,
181    pub dwarf_type: Option<TypeInfo>,
182    pub availability: Availability,
183    pub lowering: VariableLoweringPlan,
184    pub materialization: VariableMaterialization,
185}
186
187/// Owned, PC-sensitive variable read plan before runtime-specific lowering.
188#[derive(Debug, Clone, PartialEq)]
189pub struct VariableReadPlan {
190    pub name: String,
191    pub type_name: String,
192    pub access_path: VariableAccessPath,
193    pub module_path: Option<PathBuf>,
194    pub dwarf_type: Option<TypeInfo>,
195    pub declaration: Option<DieRef>,
196    pub type_id: Option<TypeId>,
197    pub location: VariableLocation,
198    pub availability: Availability,
199    pub scope_depth: usize,
200    pub is_parameter: bool,
201    pub is_artificial: bool,
202    pub pc_range: Option<PcRange>,
203    pub inline_context: Option<InlineContextId>,
204    pub provenance: Provenance,
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq)]
208pub struct VariableAccessPath {
209    pub segments: Vec<VariableAccessSegment>,
210}
211
212impl VariableAccessPath {
213    pub fn new(segments: Vec<VariableAccessSegment>) -> Self {
214        Self { segments }
215    }
216
217    pub fn fields(fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
218        Self {
219            segments: fields
220                .into_iter()
221                .map(|field| VariableAccessSegment::Field(field.into()))
222                .collect(),
223        }
224    }
225
226    fn suffix(&self) -> String {
227        let mut suffix = String::new();
228        for segment in &self.segments {
229            match segment {
230                VariableAccessSegment::Field(field) => {
231                    suffix.push('.');
232                    suffix.push_str(field);
233                }
234                VariableAccessSegment::ArrayIndex(index) => {
235                    suffix.push('[');
236                    suffix.push_str(&index.to_string());
237                    suffix.push(']');
238                }
239                VariableAccessSegment::Dereference => suffix.push_str(".*"),
240            }
241        }
242        suffix
243    }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum VariableAccessSegment {
248    Field(String),
249    ArrayIndex(i64),
250    Dereference,
251}
252
253#[derive(Debug, thiserror::Error)]
254pub enum PlanError {
255    #[error("Variable '{name}' has no DWARF type information for access planning")]
256    MissingTypeInfo { name: String },
257
258    #[error("Unknown member '{field}' in {kind} '{type_name}' (known members: {members})")]
259    UnknownMember {
260        kind: &'static str,
261        type_name: String,
262        field: String,
263        members: String,
264    },
265
266    #[error("array access requires array or pointer type, got '{type_name}'")]
267    InvalidArrayAccess { type_name: String },
268
269    #[error("Pointer arithmetic requires a pointer or array expression, got '{type_name}'")]
270    InvalidPointerArithmetic { type_name: String },
271
272    #[error("pointer dereference requires pointer type, got '{type_name}'")]
273    InvalidPointerDereference { type_name: String },
274
275    #[error(
276        "cannot apply byte offset {offset} to value-backed aggregate location {location:?}; field/array extraction from aggregate values is not implemented"
277    )]
278    ValueBackedAggregateOffset {
279        offset: i64,
280        location: VariableLocation,
281    },
282
283    #[error("cannot dereference variable location shape {location:?}")]
284    UnsupportedDereference { location: VariableLocation },
285}
286
287impl PlanError {
288    pub fn is_value_backed_aggregate_access(&self) -> bool {
289        matches!(self, PlanError::ValueBackedAggregateOffset { .. })
290    }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294enum ElementIndexContext {
295    AccessPath,
296    PointerArithmetic,
297}
298
299impl VariableReadPlan {
300    pub fn from_visible_variable(variable: VisibleVariable, provenance: Provenance) -> Self {
301        Self {
302            name: variable.name,
303            type_name: variable.type_name,
304            access_path: VariableAccessPath::default(),
305            module_path: None,
306            dwarf_type: variable.dwarf_type,
307            declaration: variable.declaration,
308            type_id: variable.type_id,
309            location: variable.location,
310            availability: variable.availability,
311            scope_depth: variable.scope_depth,
312            is_parameter: variable.is_parameter,
313            is_artificial: variable.is_artificial,
314            pc_range: None,
315            inline_context: None,
316            provenance,
317        }
318    }
319
320    pub fn bpf_lowering_plan(&self, capabilities: &RuntimeCapabilities) -> VariableLoweringPlan {
321        if !self.availability.is_available() {
322            return VariableLoweringPlan {
323                kind: VariableLoweringKind::Unavailable,
324                availability: self.availability.clone(),
325                requirements: Vec::new(),
326                helper_mode: HelperMode::NoUserMemoryRead,
327                required_registers: Vec::new(),
328                estimated_stack_bytes: 0,
329                verifier_risk: VerifierRisk::Unsupported {
330                    reason: "variable is unavailable".to_string(),
331                },
332            };
333        }
334
335        let kind = self.location.lowering_kind();
336        let mut requirements = self.location.runtime_requirements();
337        requirements.sort_by_key(requirement_rank);
338        requirements.dedup();
339        let mut required_registers = self.location.required_registers();
340        required_registers.sort_unstable();
341        required_registers.dedup();
342        let estimated_stack_bytes = self.location.estimated_stack_bytes();
343        let helper_mode = helper_mode_for_requirements(&requirements, capabilities);
344        let verifier_risk =
345            verifier_risk_for_requirements(&requirements, estimated_stack_bytes, capabilities);
346
347        let availability = match &verifier_risk {
348            VerifierRisk::StackBudgetExceeded { estimated, max } => {
349                Availability::Unsupported(UnsupportedReason::ExpressionShape {
350                    detail: format!(
351                        "estimated BPF stack use {estimated} bytes exceeds capability limit {max}"
352                    ),
353                })
354            }
355            _ => requirements
356                .iter()
357                .find(|requirement| !capabilities.supports_requirement(requirement))
358                .cloned()
359                .map(Availability::Requires)
360                .unwrap_or(Availability::Available),
361        };
362
363        VariableLoweringPlan {
364            kind,
365            availability,
366            requirements,
367            helper_mode,
368            required_registers,
369            estimated_stack_bytes,
370            verifier_risk,
371        }
372    }
373
374    pub fn materialization_plan(
375        &self,
376        capabilities: &RuntimeCapabilities,
377    ) -> VariableMaterializationPlan {
378        let lowering = self.bpf_lowering_plan(capabilities);
379        let materialization = if !lowering.availability.is_available() {
380            VariableMaterialization::Unavailable {
381                availability: lowering.availability.clone(),
382            }
383        } else {
384            match lowering.kind {
385                VariableLoweringKind::DirectValue => {
386                    let size = planned_value_size(self.dwarf_type.as_ref());
387                    match PlannedValue::from_location(self.location.clone(), size) {
388                        Some(value) => VariableMaterialization::DirectValue { value },
389                        None => VariableMaterialization::Unavailable {
390                            availability: Availability::Unsupported(
391                                UnsupportedReason::ExpressionShape {
392                                    detail: format!(
393                                        "location {} cannot be materialized as a direct value",
394                                        self.location
395                                    ),
396                                },
397                            ),
398                        },
399                    }
400                }
401                VariableLoweringKind::UserMemoryRead => {
402                    match PlannedAddress::from_location(self.location.clone()) {
403                        Some(address) => VariableMaterialization::UserMemoryRead { address },
404                        None => VariableMaterialization::Unavailable {
405                            availability: Availability::Unsupported(
406                                UnsupportedReason::AddressClass {
407                                    detail: format!(
408                                        "location {} cannot be materialized as an address",
409                                        self.location
410                                    ),
411                                },
412                            ),
413                        },
414                    }
415                }
416                VariableLoweringKind::Composite => match &self.location {
417                    VariableLocation::Pieces(pieces) => VariableMaterialization::Composite {
418                        pieces: pieces.clone(),
419                    },
420                    _ => VariableMaterialization::Unavailable {
421                        availability: Availability::Unsupported(
422                            UnsupportedReason::ExpressionShape {
423                                detail: "composite lowering without piece locations".to_string(),
424                            },
425                        ),
426                    },
427                },
428                VariableLoweringKind::Unavailable => VariableMaterialization::Unavailable {
429                    availability: lowering.availability.clone(),
430                },
431            }
432        };
433
434        VariableMaterializationPlan {
435            name: self.name.clone(),
436            type_name: self.type_name.clone(),
437            access_path: self.access_path.clone(),
438            module_path: self.module_path.clone(),
439            dwarf_type: self.dwarf_type.clone(),
440            availability: lowering.availability.clone(),
441            lowering,
442            materialization,
443        }
444    }
445
446    pub fn lvalue_address_plan(&self) -> LvalueAddressPlan {
447        if !self.availability.is_available() {
448            LvalueAddressPlan::Unavailable {
449                availability: self.availability.clone(),
450            }
451        } else {
452            lvalue_address_materialization(&self.name, &self.location)
453        }
454    }
455
456    pub fn plan_access_path(&self, path: &VariableAccessPath) -> Result<Self> {
457        let mut plan = self.clone();
458        for segment in &path.segments {
459            plan = plan.plan_access_segment(segment)?;
460        }
461
462        plan.access_path.segments.extend(path.segments.clone());
463        plan.name.push_str(&path.suffix());
464        Ok(plan)
465    }
466
467    /// Plan pointer-style element access for expressions like `ptr +/- K`.
468    ///
469    /// This keeps pointer dereference and element-size scaling in the DWARF
470    /// semantic layer instead of making compiler lowering rewrite locations.
471    pub fn plan_pointer_element_index(&self, index: i64) -> Result<Self> {
472        let dwarf_type = self
473            .dwarf_type
474            .clone()
475            .ok_or_else(|| PlanError::MissingTypeInfo {
476                name: self.name.clone(),
477            })?;
478        let mut plan =
479            self.plan_element_index(&dwarf_type, index, ElementIndexContext::PointerArithmetic)?;
480        let segment = VariableAccessSegment::ArrayIndex(index);
481        plan.access_path.segments.push(segment.clone());
482        plan.name
483            .push_str(&VariableAccessPath::new(vec![segment]).suffix());
484        Ok(plan)
485    }
486
487    fn plan_access_segment(&self, segment: &VariableAccessSegment) -> Result<Self> {
488        let dwarf_type = self
489            .dwarf_type
490            .clone()
491            .ok_or_else(|| PlanError::MissingTypeInfo {
492                name: self.name.clone(),
493            })?;
494
495        match segment {
496            VariableAccessSegment::Field(field) => self.plan_field_access(&dwarf_type, field),
497            VariableAccessSegment::ArrayIndex(index) => self.plan_array_index(&dwarf_type, *index),
498            VariableAccessSegment::Dereference => self.plan_pointer_deref(&dwarf_type),
499        }
500    }
501
502    fn plan_field_access(&self, dwarf_type: &TypeInfo, field: &str) -> Result<Self> {
503        let (base_location, aggregate_type) = match strip_type_aliases(dwarf_type) {
504            TypeInfo::PointerType { target_type, .. } => (
505                dereference_location(&self.location)?,
506                strip_type_aliases(target_type).clone(),
507            ),
508            ty => (self.location.clone(), ty.clone()),
509        };
510
511        let member = member_layout(&aggregate_type, field).map_err(|err| match err {
512            TypeLayoutError::UnknownMember {
513                kind,
514                type_name,
515                field,
516                members,
517            } => PlanError::UnknownMember {
518                kind,
519                type_name,
520                field,
521                members,
522            }
523            .into(),
524            TypeLayoutError::InvalidMemberBase { type_name } => {
525                anyhow::anyhow!("member '{}' not found on type '{}'", field, type_name)
526            }
527        })?;
528
529        let mut plan = self.clone();
530        plan.location = add_location_offset(base_location, member.offset as i64)?;
531        plan.type_name = member.member_type.type_name();
532        plan.dwarf_type = Some(member.member_type);
533        plan.type_id = None;
534        Ok(plan)
535    }
536
537    fn plan_array_index(&self, dwarf_type: &TypeInfo, index: i64) -> Result<Self> {
538        self.plan_element_index(dwarf_type, index, ElementIndexContext::AccessPath)
539    }
540
541    fn plan_element_index(
542        &self,
543        dwarf_type: &TypeInfo,
544        index: i64,
545        context: ElementIndexContext,
546    ) -> Result<Self> {
547        let base_location = match strip_type_aliases(dwarf_type) {
548            TypeInfo::ArrayType { .. } => self.location.clone(),
549            TypeInfo::PointerType { .. } => dereference_location(&self.location)?,
550            ty => {
551                let type_name = ty.type_name();
552                return Err(match context {
553                    ElementIndexContext::AccessPath => PlanError::InvalidArrayAccess { type_name },
554                    ElementIndexContext::PointerArithmetic => {
555                        PlanError::InvalidPointerArithmetic { type_name }
556                    }
557                }
558                .into());
559            }
560        };
561        let layout = indexable_element_layout(dwarf_type)
562            .expect("array and pointer types must have element layout");
563
564        let byte_offset = index.saturating_mul(layout.stride as i64);
565        let mut plan = self.clone();
566        plan.location = add_location_offset(base_location, byte_offset)?;
567        plan.type_name = layout.element_type.type_name();
568        plan.dwarf_type = Some(layout.element_type);
569        plan.type_id = None;
570        Ok(plan)
571    }
572
573    fn plan_pointer_deref(&self, dwarf_type: &TypeInfo) -> Result<Self> {
574        let target_type = match strip_type_aliases(dwarf_type) {
575            TypeInfo::PointerType { target_type, .. } => target_type.as_ref().clone(),
576            ty => {
577                return Err(PlanError::InvalidPointerDereference {
578                    type_name: ty.type_name(),
579                }
580                .into());
581            }
582        };
583
584        let mut plan = self.clone();
585        plan.location = dereference_location(&self.location)?;
586        plan.type_name = target_type.type_name();
587        plan.dwarf_type = Some(target_type);
588        plan.type_id = None;
589        Ok(plan)
590    }
591}
592
593impl PlannedValue {
594    pub fn from_location(location: VariableLocation, size: MemoryAccessSize) -> Option<Self> {
595        match location {
596            VariableLocation::RegisterValue { dwarf_reg } => {
597                Some(Self::RegisterValue { dwarf_reg, size })
598            }
599            VariableLocation::ComputedValue(steps) => {
600                if let [PlanExprOp::PushConstant(value)] = steps.as_slice() {
601                    Some(Self::Constant {
602                        value: *value,
603                        size,
604                    })
605                } else {
606                    Some(Self::RuntimeComputed {
607                        expr: RuntimeComputedExpr::value(steps),
608                        result_size: size,
609                    })
610                }
611            }
612            VariableLocation::ImplicitValue(bytes) => Some(Self::ImplicitBytes(bytes)),
613            VariableLocation::AbsoluteAddressValue(expr) => {
614                PlannedAddress::from_location(VariableLocation::AbsoluteAddressValue(expr))
615                    .map(|address| Self::AddressValue { address, size })
616            }
617            VariableLocation::Address(_)
618            | VariableLocation::RegisterAddress { .. }
619            | VariableLocation::FrameBaseRelative { .. }
620            | VariableLocation::ComputedAddress(_)
621            | VariableLocation::Pieces(_)
622            | VariableLocation::OptimizedOut
623            | VariableLocation::Unknown => None,
624        }
625    }
626}
627
628impl PlannedAddress {
629    pub fn from_location(location: VariableLocation) -> Option<Self> {
630        let (kind, origin) = match location {
631            VariableLocation::Address(expr) | VariableLocation::AbsoluteAddressValue(expr) => {
632                let origin = address_origin_for_steps(&expr.steps);
633                (PlannedAddressKind::from_steps(expr.steps), origin)
634            }
635            VariableLocation::RegisterAddress { dwarf_reg, offset } => (
636                PlannedAddressKind::RegisterOffset { dwarf_reg, offset },
637                AddressOrigin::RuntimeDerived,
638            ),
639            VariableLocation::FrameBaseRelative { offset } => (
640                PlannedAddressKind::FrameBaseRelative { offset },
641                AddressOrigin::RuntimeDerived,
642            ),
643            VariableLocation::ComputedAddress(steps) => {
644                let origin = address_origin_for_steps(&steps);
645                (PlannedAddressKind::from_steps(steps), origin)
646            }
647            VariableLocation::RegisterValue { .. }
648            | VariableLocation::ComputedValue(_)
649            | VariableLocation::ImplicitValue(_)
650            | VariableLocation::Pieces(_)
651            | VariableLocation::OptimizedOut
652            | VariableLocation::Unknown => return None,
653        };
654
655        Some(Self { kind, origin })
656    }
657
658    pub fn constant_link_time_address(&self) -> Option<u64> {
659        match (&self.origin, &self.kind) {
660            (AddressOrigin::LinkTime, PlannedAddressKind::Constant { address }) => Some(*address),
661            (AddressOrigin::LinkTime, PlannedAddressKind::RuntimeComputed { expr }) => {
662                fold_constant_steps(expr.ops())
663            }
664            _ => None,
665        }
666    }
667
668    pub fn link_time_base_and_runtime_tail(&self) -> Option<(u64, &[PlanExprOp])> {
669        if self.origin != AddressOrigin::LinkTimeBase {
670            return None;
671        }
672
673        match &self.kind {
674            PlannedAddressKind::RuntimeComputed { expr } => {
675                link_time_base_and_runtime_tail(expr.ops())
676            }
677            _ => None,
678        }
679    }
680}
681
682impl PlannedAddressKind {
683    fn from_steps(steps: Vec<PlanExprOp>) -> Self {
684        match fold_constant_steps(&steps) {
685            Some(address) => Self::Constant { address },
686            None => Self::RuntimeComputed {
687                expr: RuntimeComputedExpr::address(steps),
688            },
689        }
690    }
691}
692
693impl RuntimeCapabilities {
694    pub fn supports_requirement(&self, requirement: &RuntimeRequirement) -> bool {
695        match requirement {
696            RuntimeRequirement::CallerFrame | RuntimeRequirement::DwarfCfiRecovery => {
697                self.bounded_loops
698            }
699            RuntimeRequirement::SleepableUprobe => self.sleepable_uprobe,
700            RuntimeRequirement::UserMemoryRead => {
701                self.regular_uprobe || self.sleepable_uprobe || self.copy_from_user_task
702            }
703        }
704    }
705}
706
707fn planned_value_size(dwarf_type: Option<&TypeInfo>) -> MemoryAccessSize {
708    dwarf_type
709        .map(|ty| MemoryAccessSize::from_size(ty.size()))
710        .unwrap_or(MemoryAccessSize::U64)
711}
712
713fn lvalue_address_materialization(name: &str, location: &VariableLocation) -> LvalueAddressPlan {
714    match location {
715        VariableLocation::Address(_)
716        | VariableLocation::RegisterAddress { .. }
717        | VariableLocation::FrameBaseRelative { .. }
718        | VariableLocation::ComputedAddress(_) => {
719            match PlannedAddress::from_location(location.clone()) {
720                Some(address) => LvalueAddressPlan::Address { address },
721                None => LvalueAddressPlan::Unavailable {
722                    availability: Availability::Unsupported(UnsupportedReason::AddressClass {
723                        detail: format!(
724                            "DWARF variable '{name}' has an address-backed location that could not be planned"
725                        ),
726                    }),
727                },
728            }
729        }
730        VariableLocation::OptimizedOut => LvalueAddressPlan::Unavailable {
731            availability: Availability::OptimizedOut,
732        },
733        VariableLocation::Pieces(_) => LvalueAddressPlan::Unavailable {
734            availability: Availability::Unsupported(UnsupportedReason::ExpressionShape {
735                detail: "split variable pieces cannot be materialized as one lvalue address"
736                    .to_string(),
737            }),
738        },
739        VariableLocation::AbsoluteAddressValue(_)
740        | VariableLocation::RegisterValue { .. }
741        | VariableLocation::ComputedValue(_)
742        | VariableLocation::ImplicitValue(_) => LvalueAddressPlan::Unavailable {
743            availability: Availability::Unsupported(UnsupportedReason::AddressClass {
744                detail: "cannot take address of value-backed DWARF expression".to_string(),
745            }),
746        },
747        VariableLocation::Unknown => LvalueAddressPlan::Unavailable {
748            availability: Availability::Unsupported(UnsupportedReason::AddressClass {
749                detail: "unknown DWARF variable location".to_string(),
750            }),
751        },
752    }
753}
754
755fn address_origin_for_steps(steps: &[PlanExprOp]) -> AddressOrigin {
756    if fold_constant_steps(steps).is_some() {
757        return AddressOrigin::LinkTime;
758    }
759
760    if link_time_base_and_runtime_tail(steps).is_some() {
761        return AddressOrigin::LinkTimeBase;
762    }
763
764    if steps_reference_runtime_state(steps) {
765        AddressOrigin::RuntimeDerived
766    } else {
767        AddressOrigin::Unknown
768    }
769}
770
771fn fold_constant_steps(steps: &[PlanExprOp]) -> Option<u64> {
772    let mut const_stack: Vec<i64> = Vec::new();
773    for step in steps {
774        match step {
775            PlanExprOp::PushConstant(value) => const_stack.push(*value),
776            PlanExprOp::Add => {
777                let rhs = const_stack.pop()?;
778                let lhs = const_stack.pop()?;
779                const_stack.push(lhs.saturating_add(rhs));
780            }
781            _ => return None,
782        }
783    }
784
785    if const_stack.len() == 1 && const_stack[0] >= 0 {
786        Some(const_stack[0] as u64)
787    } else {
788        None
789    }
790}
791
792fn link_time_base_and_runtime_tail(steps: &[PlanExprOp]) -> Option<(u64, &[PlanExprOp])> {
793    let Some(PlanExprOp::PushConstant(base)) = steps.first() else {
794        return None;
795    };
796
797    if *base < 0 {
798        return None;
799    }
800
801    for step in steps.iter().skip(1) {
802        match step {
803            PlanExprOp::LoadRegister(_) => {
804                break;
805            }
806            PlanExprOp::FormTlsAddress => return None,
807            PlanExprOp::Dereference { .. } => {
808                return Some((*base as u64, &steps[1..]));
809            }
810            _ => {}
811        }
812    }
813
814    None
815}
816
817fn steps_reference_runtime_state(steps: &[PlanExprOp]) -> bool {
818    steps.iter().any(|step| match step {
819        PlanExprOp::LoadRegister(_)
820        | PlanExprOp::Dereference { .. }
821        | PlanExprOp::FormTlsAddress
822        | PlanExprOp::EntryValueLookup { .. } => true,
823        PlanExprOp::If {
824            then_branch,
825            else_branch,
826        } => {
827            steps_reference_runtime_state(then_branch) || steps_reference_runtime_state(else_branch)
828        }
829        _ => false,
830    })
831}
832
833trait VariableLocationLoweringExt {
834    fn lowering_kind(&self) -> VariableLoweringKind;
835    fn runtime_requirements(&self) -> Vec<RuntimeRequirement>;
836    fn required_registers(&self) -> Vec<u16>;
837    fn estimated_stack_bytes(&self) -> usize;
838}
839
840impl VariableLocationLoweringExt for VariableLocation {
841    fn lowering_kind(&self) -> VariableLoweringKind {
842        match self {
843            VariableLocation::Address(_)
844            | VariableLocation::RegisterAddress { .. }
845            | VariableLocation::ComputedAddress(_)
846            | VariableLocation::FrameBaseRelative { .. } => VariableLoweringKind::UserMemoryRead,
847            VariableLocation::AbsoluteAddressValue(_)
848            | VariableLocation::RegisterValue { .. }
849            | VariableLocation::ComputedValue(_)
850            | VariableLocation::ImplicitValue(_) => VariableLoweringKind::DirectValue,
851            VariableLocation::Pieces(_) => VariableLoweringKind::Composite,
852            VariableLocation::OptimizedOut | VariableLocation::Unknown => {
853                VariableLoweringKind::Unavailable
854            }
855        }
856    }
857
858    fn runtime_requirements(&self) -> Vec<RuntimeRequirement> {
859        match self {
860            VariableLocation::Address(_)
861            | VariableLocation::RegisterAddress { .. }
862            | VariableLocation::ComputedAddress(_) => {
863                let mut requirements = vec![RuntimeRequirement::UserMemoryRead];
864                if let VariableLocation::ComputedAddress(steps) = self {
865                    requirements.extend(requirements_for_steps(steps));
866                }
867                requirements
868            }
869            VariableLocation::FrameBaseRelative { .. } => vec![
870                RuntimeRequirement::DwarfCfiRecovery,
871                RuntimeRequirement::UserMemoryRead,
872            ],
873            VariableLocation::AbsoluteAddressValue(expr) => requirements_for_steps(&expr.steps),
874            VariableLocation::ComputedValue(steps) => requirements_for_steps(steps),
875            VariableLocation::Pieces(pieces) => pieces
876                .iter()
877                .flat_map(|piece| piece.location.runtime_requirements())
878                .collect(),
879            VariableLocation::RegisterValue { .. }
880            | VariableLocation::ImplicitValue(_)
881            | VariableLocation::OptimizedOut
882            | VariableLocation::Unknown => Vec::new(),
883        }
884    }
885
886    fn required_registers(&self) -> Vec<u16> {
887        match self {
888            VariableLocation::RegisterValue { dwarf_reg } => vec![*dwarf_reg],
889            VariableLocation::RegisterAddress { dwarf_reg, .. } => vec![*dwarf_reg],
890            VariableLocation::AbsoluteAddressValue(expr) => registers_for_steps(&expr.steps),
891            VariableLocation::ComputedValue(steps) | VariableLocation::ComputedAddress(steps) => {
892                registers_for_steps(steps)
893            }
894            VariableLocation::Pieces(pieces) => pieces
895                .iter()
896                .flat_map(|piece| piece.location.required_registers())
897                .collect(),
898            VariableLocation::Address(_)
899            | VariableLocation::FrameBaseRelative { .. }
900            | VariableLocation::ImplicitValue(_)
901            | VariableLocation::OptimizedOut
902            | VariableLocation::Unknown => Vec::new(),
903        }
904    }
905
906    fn estimated_stack_bytes(&self) -> usize {
907        match self {
908            VariableLocation::AbsoluteAddressValue(expr) => {
909                estimate_steps_stack_bytes(&expr.steps).max(8)
910            }
911            VariableLocation::ComputedValue(steps) | VariableLocation::ComputedAddress(steps) => {
912                estimate_steps_stack_bytes(steps)
913            }
914            VariableLocation::Pieces(pieces) => pieces
915                .iter()
916                .map(|piece| piece.location.estimated_stack_bytes())
917                .max()
918                .unwrap_or(0),
919            VariableLocation::Address(_)
920            | VariableLocation::RegisterValue { .. }
921            | VariableLocation::RegisterAddress { .. }
922            | VariableLocation::FrameBaseRelative { .. } => 8,
923            VariableLocation::ImplicitValue(bytes) => bytes.len(),
924            VariableLocation::OptimizedOut | VariableLocation::Unknown => 0,
925        }
926    }
927}
928
929fn requirements_for_steps(steps: &[PlanExprOp]) -> Vec<RuntimeRequirement> {
930    let mut requirements = Vec::new();
931    for step in steps {
932        match step {
933            PlanExprOp::Dereference { .. } => requirements.push(RuntimeRequirement::UserMemoryRead),
934            PlanExprOp::EntryValueLookup {
935                caller_pc_steps,
936                cases,
937            } => {
938                requirements.push(RuntimeRequirement::CallerFrame);
939                requirements.extend(requirements_for_steps(caller_pc_steps));
940                for case in cases {
941                    requirements.extend(requirements_for_steps(&case.value_steps));
942                }
943            }
944            PlanExprOp::If {
945                then_branch,
946                else_branch,
947            } => {
948                requirements.extend(requirements_for_steps(then_branch));
949                requirements.extend(requirements_for_steps(else_branch));
950            }
951            _ => {}
952        }
953    }
954    requirements
955}
956
957fn registers_for_steps(steps: &[PlanExprOp]) -> Vec<u16> {
958    let mut registers = Vec::new();
959    collect_registers_for_steps(steps, &mut registers);
960    registers
961}
962
963fn collect_registers_for_steps(steps: &[PlanExprOp], registers: &mut Vec<u16>) {
964    for step in steps {
965        match step {
966            PlanExprOp::LoadRegister(register) => registers.push(*register),
967            PlanExprOp::EntryValueLookup {
968                caller_pc_steps,
969                cases,
970            } => {
971                collect_registers_for_steps(caller_pc_steps, registers);
972                for case in cases {
973                    collect_registers_for_steps(&case.value_steps, registers);
974                }
975            }
976            PlanExprOp::If {
977                then_branch,
978                else_branch,
979            } => {
980                collect_registers_for_steps(then_branch, registers);
981                collect_registers_for_steps(else_branch, registers);
982            }
983            _ => {}
984        }
985    }
986}
987
988fn estimate_steps_stack_bytes(steps: &[PlanExprOp]) -> usize {
989    let nested = steps
990        .iter()
991        .map(|step| match step {
992            PlanExprOp::EntryValueLookup {
993                caller_pc_steps,
994                cases,
995            } => cases
996                .iter()
997                .map(|case| estimate_steps_stack_bytes(&case.value_steps))
998                .chain(std::iter::once(estimate_steps_stack_bytes(caller_pc_steps)))
999                .max()
1000                .unwrap_or(0),
1001            PlanExprOp::If {
1002                then_branch,
1003                else_branch,
1004            } => {
1005                estimate_steps_stack_bytes(then_branch).max(estimate_steps_stack_bytes(else_branch))
1006            }
1007            _ => 0,
1008        })
1009        .max()
1010        .unwrap_or(0);
1011    steps.len().saturating_mul(8).max(nested)
1012}
1013
1014fn helper_mode_for_requirements(
1015    requirements: &[RuntimeRequirement],
1016    capabilities: &RuntimeCapabilities,
1017) -> HelperMode {
1018    if !requirements.contains(&RuntimeRequirement::UserMemoryRead) {
1019        HelperMode::NoUserMemoryRead
1020    } else if capabilities.sleepable_uprobe && capabilities.copy_from_user_task {
1021        HelperMode::CopyFromUserTask
1022    } else {
1023        HelperMode::ProbeReadUser
1024    }
1025}
1026
1027fn verifier_risk_for_requirements(
1028    requirements: &[RuntimeRequirement],
1029    estimated_stack_bytes: usize,
1030    capabilities: &RuntimeCapabilities,
1031) -> VerifierRisk {
1032    if estimated_stack_bytes > capabilities.max_bpf_stack_bytes {
1033        return VerifierRisk::StackBudgetExceeded {
1034            estimated: estimated_stack_bytes,
1035            max: capabilities.max_bpf_stack_bytes,
1036        };
1037    }
1038
1039    if requirements.iter().any(|requirement| {
1040        matches!(
1041            requirement,
1042            RuntimeRequirement::CallerFrame | RuntimeRequirement::DwarfCfiRecovery
1043        )
1044    }) {
1045        VerifierRisk::RequiresBoundedLoops
1046    } else {
1047        VerifierRisk::Low
1048    }
1049}
1050
1051fn requirement_rank(requirement: &RuntimeRequirement) -> u8 {
1052    match requirement {
1053        RuntimeRequirement::CallerFrame => 0,
1054        RuntimeRequirement::SleepableUprobe => 1,
1055        RuntimeRequirement::UserMemoryRead => 2,
1056        RuntimeRequirement::DwarfCfiRecovery => 3,
1057    }
1058}
1059
1060/// Apply a byte offset to an address-backed source variable location.
1061pub fn add_location_offset(location: VariableLocation, offset: i64) -> Result<VariableLocation> {
1062    match location {
1063        VariableLocation::Address(expr) => {
1064            Ok(VariableLocation::Address(offset_address_expr(expr, offset)))
1065        }
1066        VariableLocation::RegisterAddress {
1067            dwarf_reg,
1068            offset: base,
1069        } => Ok(VariableLocation::RegisterAddress {
1070            dwarf_reg,
1071            offset: base.saturating_add(offset),
1072        }),
1073        VariableLocation::FrameBaseRelative { offset: base } => {
1074            Ok(VariableLocation::FrameBaseRelative {
1075                offset: base.saturating_add(offset),
1076            })
1077        }
1078        VariableLocation::ComputedAddress(mut steps) => {
1079            push_add_offset(&mut steps, offset);
1080            Ok(VariableLocation::ComputedAddress(steps))
1081        }
1082        VariableLocation::OptimizedOut => Ok(VariableLocation::OptimizedOut),
1083        VariableLocation::Unknown => Ok(VariableLocation::Unknown),
1084        VariableLocation::AbsoluteAddressValue(_)
1085        | VariableLocation::RegisterValue { .. }
1086        | VariableLocation::ComputedValue(_)
1087        | VariableLocation::ImplicitValue(_)
1088        | VariableLocation::Pieces(_) => {
1089            Err(PlanError::ValueBackedAggregateOffset { offset, location }.into())
1090        }
1091    }
1092}
1093
1094fn offset_address_expr(mut expr: AddressExpr, offset: i64) -> AddressExpr {
1095    if let [PlanExprOp::PushConstant(base)] = expr.steps.as_mut_slice() {
1096        *base = base.saturating_add(offset);
1097        return expr;
1098    }
1099    push_add_offset(&mut expr.steps, offset);
1100    expr
1101}
1102
1103fn push_add_offset(steps: &mut Vec<PlanExprOp>, offset: i64) {
1104    if offset != 0 {
1105        steps.push(PlanExprOp::PushConstant(offset));
1106        steps.push(PlanExprOp::Add);
1107    }
1108}
1109
1110/// Turn a pointer-valued source variable location into its pointee location.
1111pub fn dereference_location(location: &VariableLocation) -> Result<VariableLocation> {
1112    match location {
1113        VariableLocation::AbsoluteAddressValue(expr) => Ok(VariableLocation::Address(expr.clone())),
1114        VariableLocation::RegisterValue { dwarf_reg } => {
1115            Ok(VariableLocation::ComputedAddress(vec![
1116                PlanExprOp::LoadRegister(*dwarf_reg),
1117            ]))
1118        }
1119        VariableLocation::ComputedValue(steps) => {
1120            Ok(VariableLocation::ComputedAddress(steps.clone()))
1121        }
1122        VariableLocation::ImplicitValue(bytes) => {
1123            let mut address = 0u64;
1124            for (index, byte) in bytes.iter().take(8).enumerate() {
1125                address |= (*byte as u64) << (index * 8);
1126            }
1127            Ok(VariableLocation::Address(AddressExpr::constant(address)))
1128        }
1129        VariableLocation::Address(expr) => {
1130            let mut steps = expr.steps.clone();
1131            steps.push(PlanExprOp::Dereference {
1132                size: MemoryAccessSize::U64,
1133            });
1134            Ok(VariableLocation::ComputedAddress(steps))
1135        }
1136        VariableLocation::RegisterAddress { dwarf_reg, offset } => {
1137            let mut steps = vec![PlanExprOp::LoadRegister(*dwarf_reg)];
1138            push_add_offset(&mut steps, *offset);
1139            steps.push(PlanExprOp::Dereference {
1140                size: MemoryAccessSize::U64,
1141            });
1142            Ok(VariableLocation::ComputedAddress(steps))
1143        }
1144        VariableLocation::ComputedAddress(steps) => {
1145            let mut steps = steps.clone();
1146            steps.push(PlanExprOp::Dereference {
1147                size: MemoryAccessSize::U64,
1148            });
1149            Ok(VariableLocation::ComputedAddress(steps))
1150        }
1151        VariableLocation::OptimizedOut => Ok(VariableLocation::OptimizedOut),
1152        VariableLocation::Unknown => Ok(VariableLocation::Unknown),
1153        VariableLocation::FrameBaseRelative { .. } | VariableLocation::Pieces(_) => {
1154            Err(PlanError::UnsupportedDereference {
1155                location: location.clone(),
1156            }
1157            .into())
1158        }
1159    }
1160}
1161
1162#[derive(Debug, Clone, PartialEq)]
1163pub struct VariablePlan {
1164    pub variable_id: VariableId,
1165    pub name: String,
1166    pub ty: TypeId,
1167    pub declaration: DieRef,
1168    pub pc_range: Option<PcRange>,
1169    pub inline_context: Option<InlineContextId>,
1170    pub location: VariableLocation,
1171    pub availability: Availability,
1172    pub provenance: Provenance,
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177    use super::*;
1178    use crate::core::{AddressExpr, EntryValueCase, MemoryAccessSize, TargetArch};
1179    use crate::StructMember;
1180
1181    fn capabilities(regular_uprobe: bool) -> RuntimeCapabilities {
1182        RuntimeCapabilities {
1183            regular_uprobe,
1184            sleepable_uprobe: false,
1185            uprobe_multi: false,
1186            copy_from_user_task: false,
1187            max_bpf_stack_bytes: 512,
1188            bounded_loops: true,
1189            arch: TargetArch::X86_64,
1190        }
1191    }
1192
1193    fn read_plan(location: VariableLocation) -> VariableReadPlan {
1194        VariableReadPlan {
1195            name: "value".to_string(),
1196            type_name: "int".to_string(),
1197            access_path: VariableAccessPath::default(),
1198            module_path: None,
1199            dwarf_type: None,
1200            declaration: None,
1201            type_id: None,
1202            location,
1203            availability: Availability::Available,
1204            scope_depth: 0,
1205            is_parameter: false,
1206            is_artificial: false,
1207            pc_range: None,
1208            inline_context: None,
1209            provenance: Provenance::DirectDie,
1210        }
1211    }
1212
1213    fn typed_read_plan(location: VariableLocation, dwarf_type: TypeInfo) -> VariableReadPlan {
1214        VariableReadPlan {
1215            type_name: dwarf_type.type_name(),
1216            dwarf_type: Some(dwarf_type),
1217            ..read_plan(location)
1218        }
1219    }
1220
1221    #[test]
1222    fn lvalue_address_plan_accepts_address_locations_without_type_info() {
1223        let plan = read_plan(VariableLocation::RegisterAddress {
1224            dwarf_reg: 6,
1225            offset: -16,
1226        });
1227
1228        let lvalue = plan.lvalue_address_plan();
1229
1230        assert_eq!(
1231            lvalue,
1232            LvalueAddressPlan::Address {
1233                address: PlannedAddress {
1234                    kind: PlannedAddressKind::RegisterOffset {
1235                        dwarf_reg: 6,
1236                        offset: -16
1237                    },
1238                    origin: AddressOrigin::RuntimeDerived,
1239                }
1240            }
1241        );
1242    }
1243
1244    #[test]
1245    fn lvalue_address_plan_rejects_value_backed_locations() {
1246        let plan = read_plan(VariableLocation::RegisterValue { dwarf_reg: 0 });
1247
1248        let lvalue = plan.lvalue_address_plan();
1249
1250        assert!(matches!(
1251            lvalue,
1252            LvalueAddressPlan::Unavailable {
1253                availability: Availability::Unsupported(UnsupportedReason::AddressClass { .. })
1254            }
1255        ));
1256        match lvalue {
1257            LvalueAddressPlan::Unavailable {
1258                availability: Availability::Unsupported(UnsupportedReason::AddressClass { detail }),
1259            } => {
1260                assert!(detail.contains("value-backed"));
1261            }
1262            other => panic!("unexpected lvalue availability: {other:?}"),
1263        }
1264    }
1265
1266    #[test]
1267    fn lvalue_address_plan_rejects_absolute_address_values() {
1268        let plan = read_plan(VariableLocation::AbsoluteAddressValue(
1269            AddressExpr::constant(0x2000),
1270        ));
1271
1272        let lvalue = plan.lvalue_address_plan();
1273
1274        match lvalue {
1275            LvalueAddressPlan::Unavailable {
1276                availability: Availability::Unsupported(UnsupportedReason::AddressClass { detail }),
1277            } => {
1278                assert!(detail.contains("value-backed"));
1279            }
1280            other => panic!("unexpected lvalue availability: {other:?}"),
1281        }
1282    }
1283
1284    #[test]
1285    fn lvalue_address_plan_rejects_piece_locations() {
1286        let plan = read_plan(VariableLocation::Pieces(vec![PieceLocation {
1287            bit_offset: 0,
1288            bit_size: 32,
1289            location: Box::new(VariableLocation::RegisterValue { dwarf_reg: 0 }),
1290        }]));
1291
1292        let lvalue = plan.lvalue_address_plan();
1293
1294        match lvalue {
1295            LvalueAddressPlan::Unavailable {
1296                availability:
1297                    Availability::Unsupported(UnsupportedReason::ExpressionShape { detail }),
1298            } => {
1299                assert!(detail.contains("split variable pieces"));
1300            }
1301            other => panic!("unexpected lvalue availability: {other:?}"),
1302        }
1303    }
1304
1305    #[test]
1306    fn lvalue_address_plan_preserves_optimized_out_availability() {
1307        let plan = VariableReadPlan {
1308            availability: Availability::OptimizedOut,
1309            ..read_plan(VariableLocation::OptimizedOut)
1310        };
1311
1312        let lvalue = plan.lvalue_address_plan();
1313
1314        assert_eq!(
1315            lvalue,
1316            LvalueAddressPlan::Unavailable {
1317                availability: Availability::OptimizedOut
1318            }
1319        );
1320    }
1321
1322    #[test]
1323    fn register_value_lowers_without_runtime_requirements() {
1324        let plan = read_plan(VariableLocation::RegisterValue { dwarf_reg: 0 });
1325        let lowering = plan.bpf_lowering_plan(&capabilities(false));
1326
1327        assert_eq!(lowering.kind, VariableLoweringKind::DirectValue);
1328        assert_eq!(lowering.availability, Availability::Available);
1329        assert!(lowering.requirements.is_empty());
1330    }
1331
1332    #[test]
1333    fn memory_location_requires_user_memory_read() {
1334        let plan = read_plan(VariableLocation::Address(AddressExpr::constant(0x1000)));
1335        let lowering = plan.bpf_lowering_plan(&capabilities(false));
1336
1337        assert_eq!(lowering.kind, VariableLoweringKind::UserMemoryRead);
1338        assert_eq!(
1339            lowering.availability,
1340            Availability::Requires(RuntimeRequirement::UserMemoryRead)
1341        );
1342        assert_eq!(
1343            lowering.requirements,
1344            vec![RuntimeRequirement::UserMemoryRead]
1345        );
1346    }
1347
1348    #[test]
1349    fn memory_location_is_available_with_regular_uprobe() {
1350        let plan = read_plan(VariableLocation::Address(AddressExpr::constant(0x1000)));
1351        let lowering = plan.bpf_lowering_plan(&capabilities(true));
1352
1353        assert_eq!(lowering.kind, VariableLoweringKind::UserMemoryRead);
1354        assert_eq!(lowering.availability, Availability::Available);
1355        assert_eq!(lowering.helper_mode, HelperMode::ProbeReadUser);
1356        assert_eq!(lowering.verifier_risk, VerifierRisk::Low);
1357        assert!(lowering.required_registers.is_empty());
1358    }
1359
1360    #[test]
1361    fn materialization_plan_preserves_link_time_address_origin() {
1362        let plan = read_plan(VariableLocation::Address(AddressExpr::constant(0x1000)));
1363        let materialized = plan.materialization_plan(&capabilities(true));
1364
1365        match materialized.materialization {
1366            VariableMaterialization::UserMemoryRead { address } => {
1367                assert_eq!(address.origin, AddressOrigin::LinkTime);
1368                assert_eq!(address.constant_link_time_address(), Some(0x1000));
1369                assert_eq!(
1370                    address.kind,
1371                    PlannedAddressKind::Constant { address: 0x1000 }
1372                );
1373            }
1374            other => panic!("unexpected materialization: {other:?}"),
1375        }
1376    }
1377
1378    #[test]
1379    fn materialization_plan_preserves_module_path_origin() {
1380        let mut plan = read_plan(VariableLocation::Address(AddressExpr::constant(0x1000)));
1381        plan.module_path = Some(PathBuf::from("/tmp/libstate.so"));
1382
1383        let materialized = plan.materialization_plan(&capabilities(true));
1384
1385        assert_eq!(
1386            materialized.module_path,
1387            Some(PathBuf::from("/tmp/libstate.so"))
1388        );
1389    }
1390
1391    #[test]
1392    fn materialization_plan_converts_register_address_to_address_kind() {
1393        let plan = read_plan(VariableLocation::RegisterAddress {
1394            dwarf_reg: 6,
1395            offset: -16,
1396        });
1397        let materialized = plan.materialization_plan(&capabilities(true));
1398
1399        match materialized.materialization {
1400            VariableMaterialization::UserMemoryRead { address } => {
1401                assert_eq!(address.origin, AddressOrigin::RuntimeDerived);
1402                assert_eq!(
1403                    address.kind,
1404                    PlannedAddressKind::RegisterOffset {
1405                        dwarf_reg: 6,
1406                        offset: -16
1407                    }
1408                );
1409            }
1410            other => panic!("unexpected materialization: {other:?}"),
1411        }
1412    }
1413
1414    #[test]
1415    fn materialization_plan_marks_static_base_before_deref() {
1416        let plan = read_plan(VariableLocation::ComputedAddress(vec![
1417            PlanExprOp::PushConstant(0x3000),
1418            PlanExprOp::Dereference {
1419                size: MemoryAccessSize::U64,
1420            },
1421            PlanExprOp::PushConstant(16),
1422            PlanExprOp::Add,
1423        ]));
1424        let materialized = plan.materialization_plan(&capabilities(true));
1425
1426        match materialized.materialization {
1427            VariableMaterialization::UserMemoryRead { address } => {
1428                assert_eq!(address.origin, AddressOrigin::LinkTimeBase);
1429                match &address.kind {
1430                    PlannedAddressKind::RuntimeComputed { expr } => {
1431                        assert_eq!(expr.kind(), RuntimeComputedKind::Address);
1432                    }
1433                    other => panic!("unexpected address kind: {other:?}"),
1434                }
1435                let (base, tail) = address
1436                    .link_time_base_and_runtime_tail()
1437                    .expect("link-time base");
1438                assert_eq!(base, 0x3000);
1439                assert_eq!(tail.len(), 3);
1440            }
1441            other => panic!("unexpected materialization: {other:?}"),
1442        }
1443    }
1444
1445    #[test]
1446    fn materialization_plan_preserves_arithmetic_before_first_deref() {
1447        let plan = read_plan(VariableLocation::ComputedAddress(vec![
1448            PlanExprOp::PushConstant(0x3000),
1449            PlanExprOp::PushConstant(8),
1450            PlanExprOp::Add,
1451            PlanExprOp::Dereference {
1452                size: MemoryAccessSize::U64,
1453            },
1454        ]));
1455        let materialized = plan.materialization_plan(&capabilities(true));
1456
1457        match materialized.materialization {
1458            VariableMaterialization::UserMemoryRead { address } => {
1459                assert_eq!(address.origin, AddressOrigin::LinkTimeBase);
1460                let (base, tail) = address
1461                    .link_time_base_and_runtime_tail()
1462                    .expect("link-time base");
1463                assert_eq!(base, 0x3000);
1464                assert_eq!(
1465                    tail,
1466                    &[
1467                        PlanExprOp::PushConstant(8),
1468                        PlanExprOp::Add,
1469                        PlanExprOp::Dereference {
1470                            size: MemoryAccessSize::U64,
1471                        },
1472                    ]
1473                );
1474            }
1475            other => panic!("unexpected materialization: {other:?}"),
1476        }
1477    }
1478
1479    #[test]
1480    fn materialization_plan_keeps_absolute_address_value_direct() {
1481        let plan = read_plan(VariableLocation::AbsoluteAddressValue(
1482            AddressExpr::constant(0x2000),
1483        ));
1484        let materialized = plan.materialization_plan(&capabilities(false));
1485
1486        match materialized.materialization {
1487            VariableMaterialization::DirectValue {
1488                value:
1489                    PlannedValue::AddressValue {
1490                        address:
1491                            PlannedAddress {
1492                                origin: AddressOrigin::LinkTime,
1493                                kind: PlannedAddressKind::Constant { address: 0x2000 },
1494                                ..
1495                            },
1496                        size: MemoryAccessSize::U64,
1497                    },
1498            } => {}
1499            VariableMaterialization::DirectValue { value } => {
1500                panic!("unexpected direct value: {value:?}");
1501            }
1502            other => panic!("unexpected materialization: {other:?}"),
1503        }
1504    }
1505
1506    #[test]
1507    fn materialization_plan_converts_constant_direct_value() {
1508        let plan = read_plan(VariableLocation::ComputedValue(vec![
1509            PlanExprOp::PushConstant(42),
1510        ]));
1511        let materialized = plan.materialization_plan(&capabilities(false));
1512
1513        match materialized.materialization {
1514            VariableMaterialization::DirectValue {
1515                value:
1516                    PlannedValue::Constant {
1517                        value: 42,
1518                        size: MemoryAccessSize::U64,
1519                    },
1520            } => {}
1521            other => panic!("unexpected materialization: {other:?}"),
1522        }
1523    }
1524
1525    #[test]
1526    fn materialization_plan_records_direct_value_size_from_type() {
1527        let byte_type = TypeInfo::BaseType {
1528            name: "uint8_t".to_string(),
1529            size: 1,
1530            encoding: gimli::constants::DW_ATE_unsigned.0 as u16,
1531        };
1532        let plan = typed_read_plan(
1533            VariableLocation::ComputedValue(vec![
1534                PlanExprOp::LoadRegister(0),
1535                PlanExprOp::PushConstant(1),
1536                PlanExprOp::Add,
1537            ]),
1538            byte_type,
1539        );
1540        let materialized = plan.materialization_plan(&capabilities(false));
1541
1542        match materialized.materialization {
1543            VariableMaterialization::DirectValue {
1544                value:
1545                    PlannedValue::RuntimeComputed {
1546                        ref expr,
1547                        result_size: MemoryAccessSize::U8,
1548                        ..
1549                    },
1550            } => {
1551                assert_eq!(expr.kind(), RuntimeComputedKind::Value);
1552            }
1553            other => panic!("unexpected materialization: {other:?}"),
1554        }
1555    }
1556
1557    #[test]
1558    fn materialization_plan_converts_register_direct_value() {
1559        let plan = read_plan(VariableLocation::RegisterValue { dwarf_reg: 6 });
1560        let materialized = plan.materialization_plan(&capabilities(false));
1561
1562        match materialized.materialization {
1563            VariableMaterialization::DirectValue {
1564                value:
1565                    PlannedValue::RegisterValue {
1566                        dwarf_reg: 6,
1567                        size: MemoryAccessSize::U64,
1568                    },
1569            } => {}
1570            other => panic!("unexpected materialization: {other:?}"),
1571        }
1572    }
1573
1574    #[test]
1575    fn materialization_plan_surfaces_piece_locations_without_first_piece_fallback() {
1576        let plan = read_plan(VariableLocation::Pieces(vec![PieceLocation {
1577            bit_offset: 0,
1578            bit_size: 32,
1579            location: Box::new(VariableLocation::RegisterValue { dwarf_reg: 0 }),
1580        }]));
1581        let materialized = plan.materialization_plan(&capabilities(true));
1582
1583        match materialized.materialization {
1584            VariableMaterialization::Composite { pieces } => {
1585                assert_eq!(pieces.len(), 1);
1586            }
1587            other => panic!("unexpected materialization: {other:?}"),
1588        }
1589    }
1590
1591    #[test]
1592    fn absolute_address_value_lowers_without_user_memory_read() {
1593        let plan = read_plan(VariableLocation::AbsoluteAddressValue(
1594            AddressExpr::constant(0x1000),
1595        ));
1596        let lowering = plan.bpf_lowering_plan(&capabilities(false));
1597
1598        assert_eq!(lowering.kind, VariableLoweringKind::DirectValue);
1599        assert_eq!(lowering.availability, Availability::Available);
1600        assert!(lowering.requirements.is_empty());
1601    }
1602
1603    #[test]
1604    fn memory_location_prefers_copy_from_user_task_when_available() {
1605        let mut capabilities = capabilities(false);
1606        capabilities.sleepable_uprobe = true;
1607        capabilities.copy_from_user_task = true;
1608        let plan = read_plan(VariableLocation::Address(AddressExpr::constant(0x1000)));
1609        let lowering = plan.bpf_lowering_plan(&capabilities);
1610
1611        assert_eq!(lowering.availability, Availability::Available);
1612        assert_eq!(lowering.helper_mode, HelperMode::CopyFromUserTask);
1613    }
1614
1615    #[test]
1616    fn register_address_records_required_register() {
1617        let plan = read_plan(VariableLocation::RegisterAddress {
1618            dwarf_reg: 6,
1619            offset: -16,
1620        });
1621        let lowering = plan.bpf_lowering_plan(&capabilities(true));
1622
1623        assert_eq!(lowering.required_registers, vec![6]);
1624        assert_eq!(lowering.estimated_stack_bytes, 8);
1625    }
1626
1627    #[test]
1628    fn entry_value_steps_surface_caller_frame_and_memory_requirements() {
1629        let plan = read_plan(VariableLocation::ComputedValue(vec![
1630            PlanExprOp::EntryValueLookup {
1631                caller_pc_steps: vec![
1632                    PlanExprOp::LoadRegister(7),
1633                    PlanExprOp::Dereference {
1634                        size: MemoryAccessSize::U64,
1635                    },
1636                ],
1637                cases: vec![EntryValueCase {
1638                    caller_return_pc: 0x10,
1639                    value_steps: vec![PlanExprOp::LoadRegister(5)],
1640                }],
1641            },
1642        ]));
1643        let lowering = plan.bpf_lowering_plan(&capabilities(true));
1644
1645        assert_eq!(lowering.availability, Availability::Available);
1646        assert_eq!(
1647            lowering.requirements,
1648            vec![
1649                RuntimeRequirement::CallerFrame,
1650                RuntimeRequirement::UserMemoryRead
1651            ]
1652        );
1653        assert_eq!(lowering.required_registers, vec![5, 7]);
1654        assert_eq!(lowering.verifier_risk, VerifierRisk::RequiresBoundedLoops);
1655    }
1656
1657    #[test]
1658    fn stack_budget_excess_reports_unsupported_availability() {
1659        let mut capabilities = capabilities(true);
1660        capabilities.max_bpf_stack_bytes = 16;
1661        let plan = read_plan(VariableLocation::ComputedValue(vec![
1662            PlanExprOp::PushConstant(1);
1663            8
1664        ]));
1665        let lowering = plan.bpf_lowering_plan(&capabilities);
1666
1667        assert!(matches!(
1668            lowering.availability,
1669            Availability::Unsupported(UnsupportedReason::ExpressionShape { .. })
1670        ));
1671        assert_eq!(
1672            lowering.verifier_risk,
1673            VerifierRisk::StackBudgetExceeded {
1674                estimated: 64,
1675                max: 16,
1676            }
1677        );
1678    }
1679
1680    #[test]
1681    fn field_access_adds_member_offset_and_type() {
1682        let int_type = TypeInfo::BaseType {
1683            name: "int".to_string(),
1684            size: 4,
1685            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1686        };
1687        let plan = typed_read_plan(
1688            VariableLocation::RegisterAddress {
1689                dwarf_reg: 6,
1690                offset: -32,
1691            },
1692            TypeInfo::StructType {
1693                name: "Request".to_string(),
1694                size: 16,
1695                members: vec![StructMember {
1696                    name: "fd".to_string(),
1697                    member_type: int_type.clone(),
1698                    offset: 12,
1699                    bit_offset: None,
1700                    bit_size: None,
1701                }],
1702            },
1703        );
1704
1705        let access = VariableAccessPath::fields(["fd"]);
1706        let planned = plan.plan_access_path(&access).expect("field access");
1707
1708        assert_eq!(planned.name, "value.fd");
1709        assert_eq!(planned.access_path, access);
1710        assert_eq!(planned.dwarf_type, Some(int_type));
1711        assert_eq!(
1712            planned.location,
1713            VariableLocation::RegisterAddress {
1714                dwarf_reg: 6,
1715                offset: -20,
1716            }
1717        );
1718        assert_eq!(
1719            planned
1720                .materialization_plan(&capabilities(true))
1721                .access_path
1722                .segments,
1723            vec![VariableAccessSegment::Field("fd".to_string())]
1724        );
1725    }
1726
1727    #[test]
1728    fn field_access_unknown_member_reports_known_members() {
1729        let int_type = TypeInfo::BaseType {
1730            name: "int".to_string(),
1731            size: 4,
1732            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1733        };
1734        let plan = typed_read_plan(
1735            VariableLocation::Address(AddressExpr::constant(0x1000)),
1736            TypeInfo::StructType {
1737                name: "Request".to_string(),
1738                size: 8,
1739                members: vec![
1740                    StructMember {
1741                        name: "fd".to_string(),
1742                        member_type: int_type.clone(),
1743                        offset: 0,
1744                        bit_offset: None,
1745                        bit_size: None,
1746                    },
1747                    StructMember {
1748                        name: "flags".to_string(),
1749                        member_type: int_type,
1750                        offset: 4,
1751                        bit_offset: None,
1752                        bit_size: None,
1753                    },
1754                ],
1755            },
1756        );
1757
1758        let err = plan
1759            .plan_access_path(&VariableAccessPath::fields(["missing"]))
1760            .expect_err("unknown member should fail");
1761
1762        assert_eq!(
1763            err.to_string(),
1764            "Unknown member 'missing' in struct 'Request' (known members: fd, flags)"
1765        );
1766    }
1767
1768    #[test]
1769    fn field_access_folds_constant_address_offsets() {
1770        let int_type = TypeInfo::BaseType {
1771            name: "int".to_string(),
1772            size: 4,
1773            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1774        };
1775        let plan = typed_read_plan(
1776            VariableLocation::Address(AddressExpr::constant(0x1000)),
1777            TypeInfo::StructType {
1778                name: "Request".to_string(),
1779                size: 16,
1780                members: vec![StructMember {
1781                    name: "fd".to_string(),
1782                    member_type: int_type,
1783                    offset: 12,
1784                    bit_offset: None,
1785                    bit_size: None,
1786                }],
1787            },
1788        );
1789
1790        let planned = plan
1791            .plan_access_path(&VariableAccessPath::fields(["fd"]))
1792            .expect("field access");
1793
1794        assert_eq!(
1795            planned.location,
1796            VariableLocation::Address(AddressExpr::constant(0x100c))
1797        );
1798    }
1799
1800    #[test]
1801    fn field_access_rejects_value_backed_aggregates() {
1802        let int_type = TypeInfo::BaseType {
1803            name: "int".to_string(),
1804            size: 4,
1805            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1806        };
1807        let struct_type = TypeInfo::StructType {
1808            name: "Pair".to_string(),
1809            size: 8,
1810            members: vec![StructMember {
1811                name: "b".to_string(),
1812                member_type: int_type,
1813                offset: 4,
1814                bit_offset: None,
1815                bit_size: None,
1816            }],
1817        };
1818        let access = VariableAccessPath::fields(["b"]);
1819
1820        for location in [
1821            VariableLocation::AbsoluteAddressValue(AddressExpr::constant(0x1000)),
1822            VariableLocation::RegisterValue { dwarf_reg: 0 },
1823            VariableLocation::ComputedValue(vec![PlanExprOp::LoadRegister(0)]),
1824        ] {
1825            let plan = typed_read_plan(location, struct_type.clone());
1826            let err = plan
1827                .plan_access_path(&access)
1828                .expect_err("value-backed aggregate field access should fail");
1829            assert!(
1830                err.downcast_ref::<PlanError>()
1831                    .is_some_and(PlanError::is_value_backed_aggregate_access),
1832                "unexpected error: {err}"
1833            );
1834        }
1835    }
1836
1837    #[test]
1838    fn array_index_rejects_value_backed_aggregates() {
1839        let int_type = TypeInfo::BaseType {
1840            name: "int".to_string(),
1841            size: 4,
1842            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1843        };
1844        let array_type = TypeInfo::ArrayType {
1845            element_type: Box::new(int_type),
1846            element_count: Some(2),
1847            total_size: Some(8),
1848        };
1849        let access = VariableAccessPath::new(vec![VariableAccessSegment::ArrayIndex(1)]);
1850
1851        for location in [
1852            VariableLocation::AbsoluteAddressValue(AddressExpr::constant(0x1000)),
1853            VariableLocation::RegisterValue { dwarf_reg: 0 },
1854            VariableLocation::ComputedValue(vec![PlanExprOp::LoadRegister(0)]),
1855        ] {
1856            let plan = typed_read_plan(location, array_type.clone());
1857            let err = plan
1858                .plan_access_path(&access)
1859                .expect_err("value-backed aggregate array access should fail");
1860            assert!(
1861                err.downcast_ref::<PlanError>()
1862                    .is_some_and(PlanError::is_value_backed_aggregate_access),
1863                "unexpected error: {err}"
1864            );
1865        }
1866    }
1867
1868    #[test]
1869    fn pointer_field_access_dereferences_then_offsets() {
1870        let int_type = TypeInfo::BaseType {
1871            name: "int".to_string(),
1872            size: 4,
1873            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1874        };
1875        let struct_type = TypeInfo::StructType {
1876            name: "Node".to_string(),
1877            size: 16,
1878            members: vec![StructMember {
1879                name: "value".to_string(),
1880                member_type: int_type,
1881                offset: 8,
1882                bit_offset: None,
1883                bit_size: None,
1884            }],
1885        };
1886        let plan = typed_read_plan(
1887            VariableLocation::RegisterValue { dwarf_reg: 5 },
1888            TypeInfo::PointerType {
1889                target_type: Box::new(struct_type),
1890                size: 8,
1891            },
1892        );
1893
1894        let access = VariableAccessPath::fields(["value"]);
1895        let planned = plan.plan_access_path(&access).expect("pointer field");
1896
1897        assert_eq!(
1898            planned.location,
1899            VariableLocation::ComputedAddress(vec![
1900                PlanExprOp::LoadRegister(5),
1901                PlanExprOp::PushConstant(8),
1902                PlanExprOp::Add,
1903            ])
1904        );
1905    }
1906
1907    #[test]
1908    fn pointer_field_access_from_absolute_address_value_rebases_memory_location() {
1909        let int_type = TypeInfo::BaseType {
1910            name: "int".to_string(),
1911            size: 4,
1912            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1913        };
1914        let struct_type = TypeInfo::StructType {
1915            name: "Node".to_string(),
1916            size: 16,
1917            members: vec![StructMember {
1918                name: "value".to_string(),
1919                member_type: int_type,
1920                offset: 8,
1921                bit_offset: None,
1922                bit_size: None,
1923            }],
1924        };
1925        let plan = typed_read_plan(
1926            VariableLocation::AbsoluteAddressValue(AddressExpr::constant(0x1000)),
1927            TypeInfo::PointerType {
1928                target_type: Box::new(struct_type),
1929                size: 8,
1930            },
1931        );
1932
1933        let planned = plan
1934            .plan_access_path(&VariableAccessPath::fields(["value"]))
1935            .expect("pointer field");
1936
1937        assert_eq!(
1938            planned.location,
1939            VariableLocation::Address(AddressExpr::constant(0x1008))
1940        );
1941    }
1942
1943    #[test]
1944    fn pointer_field_access_from_computed_value_uses_value_as_address() {
1945        let int_type = TypeInfo::BaseType {
1946            name: "int".to_string(),
1947            size: 4,
1948            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1949        };
1950        let struct_type = TypeInfo::StructType {
1951            name: "Node".to_string(),
1952            size: 16,
1953            members: vec![StructMember {
1954                name: "value".to_string(),
1955                member_type: int_type,
1956                offset: 8,
1957                bit_offset: None,
1958                bit_size: None,
1959            }],
1960        };
1961        let plan = typed_read_plan(
1962            VariableLocation::ComputedValue(vec![PlanExprOp::PushConstant(0x2000)]),
1963            TypeInfo::PointerType {
1964                target_type: Box::new(struct_type),
1965                size: 8,
1966            },
1967        );
1968
1969        let planned = plan
1970            .plan_access_path(&VariableAccessPath::fields(["value"]))
1971            .expect("pointer field");
1972
1973        assert_eq!(
1974            planned.location,
1975            VariableLocation::ComputedAddress(vec![
1976                PlanExprOp::PushConstant(0x2000),
1977                PlanExprOp::PushConstant(8),
1978                PlanExprOp::Add,
1979            ])
1980        );
1981    }
1982
1983    #[test]
1984    fn pointer_element_index_is_planned_in_dwarf_semantics() {
1985        let int_type = TypeInfo::BaseType {
1986            name: "int".to_string(),
1987            size: 4,
1988            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1989        };
1990        let plan = typed_read_plan(
1991            VariableLocation::RegisterValue { dwarf_reg: 5 },
1992            TypeInfo::PointerType {
1993                target_type: Box::new(int_type),
1994                size: 8,
1995            },
1996        );
1997
1998        let planned = plan
1999            .plan_pointer_element_index(3)
2000            .expect("pointer element index");
2001
2002        assert_eq!(planned.name, "value[3]");
2003        assert_eq!(
2004            planned.location,
2005            VariableLocation::ComputedAddress(vec![
2006                PlanExprOp::LoadRegister(5),
2007                PlanExprOp::PushConstant(12),
2008                PlanExprOp::Add,
2009            ])
2010        );
2011    }
2012
2013    #[test]
2014    fn pointer_element_index_rejects_aggregate_arithmetic_with_pointer_error() {
2015        let int_type = TypeInfo::BaseType {
2016            name: "int".to_string(),
2017            size: 4,
2018            encoding: gimli::constants::DW_ATE_signed.0 as u16,
2019        };
2020        let plan = typed_read_plan(
2021            VariableLocation::Address(AddressExpr::constant(0x1000)),
2022            TypeInfo::StructType {
2023                name: "GlobalState".to_string(),
2024                size: 16,
2025                members: vec![StructMember {
2026                    name: "counter".to_string(),
2027                    member_type: int_type,
2028                    offset: 0,
2029                    bit_offset: None,
2030                    bit_size: None,
2031                }],
2032            },
2033        );
2034
2035        let err = plan
2036            .plan_pointer_element_index(1)
2037            .expect_err("struct arithmetic must be rejected");
2038        let plan_error = err
2039            .downcast_ref::<PlanError>()
2040            .expect("structured plan error");
2041        assert!(matches!(
2042            plan_error,
2043            PlanError::InvalidPointerArithmetic { type_name }
2044                if type_name == "struct GlobalState"
2045        ));
2046    }
2047
2048    #[test]
2049    fn array_index_access_uses_element_stride() {
2050        let int_type = TypeInfo::BaseType {
2051            name: "int".to_string(),
2052            size: 4,
2053            encoding: gimli::constants::DW_ATE_signed.0 as u16,
2054        };
2055        let plan = typed_read_plan(
2056            VariableLocation::Address(AddressExpr::constant(0x1000)),
2057            TypeInfo::ArrayType {
2058                element_type: Box::new(int_type),
2059                element_count: Some(8),
2060                total_size: Some(32),
2061            },
2062        );
2063
2064        let access = VariableAccessPath::new(vec![VariableAccessSegment::ArrayIndex(3)]);
2065        let planned = plan.plan_access_path(&access).expect("array index");
2066
2067        assert_eq!(planned.name, "value[3]");
2068        assert_eq!(
2069            planned.location,
2070            VariableLocation::Address(AddressExpr::constant(0x100c))
2071        );
2072    }
2073}