From ae3aabe5afce03166b8739a41897256609f28660 Mon Sep 17 00:00:00 2001 From: swananan Date: Sat, 11 Jul 2026 13:26:39 +0800 Subject: [PATCH 1/2] refactor: preserve DWARF type context Capture compilation-unit language, producer, and DWARF version so future language adapters can dispatch without coupling the compiler to Rust layouts. Preserve TypeId across member, array, pointer, and dereference projections, including cross-CU references and compiler pointer arithmetic. --- e2e-tests/tests/member_pointer_compilation.rs | 15 + ghostscope-compiler/src/ebpf/codegen/args.rs | 5 +- ghostscope-compiler/src/ebpf/dwarf_bridge.rs | 11 + .../src/ebpf/expression/runtime_address.rs | 4 +- ghostscope-dwarf/src/analyzer/mod.rs | 1 + ghostscope-dwarf/src/analyzer/plan_pc.rs | 5 + ghostscope-dwarf/src/analyzer/type_context.rs | 84 +++ ghostscope-dwarf/src/lib.rs | 7 +- ghostscope-dwarf/src/objfile/mod.rs | 1 + ghostscope-dwarf/src/objfile/type_context.rs | 575 ++++++++++++++++++ ghostscope-dwarf/src/objfile/variables.rs | 2 +- ghostscope-dwarf/src/semantics/mod.rs | 8 +- .../src/semantics/type_context.rs | 132 ++++ 13 files changed, 839 insertions(+), 11 deletions(-) create mode 100644 ghostscope-dwarf/src/analyzer/type_context.rs create mode 100644 ghostscope-dwarf/src/objfile/type_context.rs create mode 100644 ghostscope-dwarf/src/semantics/type_context.rs diff --git a/e2e-tests/tests/member_pointer_compilation.rs b/e2e-tests/tests/member_pointer_compilation.rs index 79b8a81b..e76cb116 100644 --- a/e2e-tests/tests/member_pointer_compilation.rs +++ b/e2e-tests/tests/member_pointer_compilation.rs @@ -290,6 +290,16 @@ async fn test_plan_variable_by_name_uses_pc_context() -> anyhow::Result<()> { assert_eq!(id_header_pos.name, "r.header_in.pos"); assert!(id_header_pos.availability.is_available()); assert!(id_header_pos.dwarf_type.is_some()); + assert!(id_header_pos.type_id.is_some()); + + let semantic_type = analyzer + .semantic_type_for_plan(&id_header_pos)? + .ok_or_else(|| anyhow::anyhow!("expected semantic type for 'r.header_in.pos'"))?; + assert_eq!(semantic_type.id, id_header_pos.type_id); + assert_eq!( + semantic_type.origin.map(|origin| origin.language), + Some(ghostscope_dwarf::SourceLanguage::C) + ); let header_pos = analyzer .plan_variable_access_by_name( @@ -308,6 +318,11 @@ async fn test_plan_variable_by_name_uses_pc_context() -> anyhow::Result<()> { header_pos.dwarf_type.is_some(), "access plan should carry final member type: {header_pos:?}" ); + assert_eq!(header_pos.type_id, id_header_pos.type_id); + + let indexed = analyzer.plan_pointer_element_index(&plan, 1)?; + assert!(indexed.type_id.is_some(), "indexed plan: {indexed:?}"); + assert_ne!(indexed.type_id, plan.type_id); Ok(()) } diff --git a/ghostscope-compiler/src/ebpf/codegen/args.rs b/ghostscope-compiler/src/ebpf/codegen/args.rs index 16b73d24..7f7a2fdd 100644 --- a/ghostscope-compiler/src/ebpf/codegen/args.rs +++ b/ghostscope-compiler/src/ebpf/codegen/args.rs @@ -516,9 +516,8 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { .as_ref() .is_some_and(ghostscope_dwarf::is_c_pointer_or_array_type) { - let pointed_plan = var - .plan_pointer_element_index(index) - .map_err(|err| CodeGenError::DwarfError(err.to_string()))?; + let pointed_plan = + self.plan_dwarf_pointer_element_index(&var, index)?; let pc_address = self.get_compile_time_context()?.pc_address; let materialized = self .variable_read_plan_to_materialization(pointed_plan, pc_address)?; diff --git a/ghostscope-compiler/src/ebpf/dwarf_bridge.rs b/ghostscope-compiler/src/ebpf/dwarf_bridge.rs index 1a87eab7..5dfadf25 100644 --- a/ghostscope-compiler/src/ebpf/dwarf_bridge.rs +++ b/ghostscope-compiler/src/ebpf/dwarf_bridge.rs @@ -1533,6 +1533,17 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { self.query_dwarf_for_variable_plan(var_name) } + pub(super) fn plan_dwarf_pointer_element_index( + &self, + plan: &VariableReadPlan, + index: i64, + ) -> Result { + self.process_analyzer + .ok_or_else(|| CodeGenError::DwarfError("No DWARF analyzer available".to_string()))? + .plan_pointer_element_index(plan, index) + .map_err(|err| CodeGenError::DwarfError(err.to_string())) + } + fn query_dwarf_for_pc_access_plan( &mut self, base_name: &str, diff --git a/ghostscope-compiler/src/ebpf/expression/runtime_address.rs b/ghostscope-compiler/src/ebpf/expression/runtime_address.rs index ebe9b58e..5e55ad78 100644 --- a/ghostscope-compiler/src/ebpf/expression/runtime_address.rs +++ b/ghostscope-compiler/src/ebpf/expression/runtime_address.rs @@ -367,9 +367,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { .map(|lvalue| lvalue.address); } else if let Some(var) = self.query_dwarf_for_complex_expr(&ptr_side)? { if var.dwarf_type.is_some() { - let pointed_plan = var - .plan_pointer_element_index(index) - .map_err(|err| CodeGenError::DwarfError(err.to_string()))?; + let pointed_plan = self.plan_dwarf_pointer_element_index(&var, index)?; let status_ptr = if self.condition_context_active { Some(self.get_or_create_cond_error_global()) } else { diff --git a/ghostscope-dwarf/src/analyzer/mod.rs b/ghostscope-dwarf/src/analyzer/mod.rs index 8f0e2745..5504a880 100644 --- a/ghostscope-dwarf/src/analyzer/mod.rs +++ b/ghostscope-dwarf/src/analyzer/mod.rs @@ -22,6 +22,7 @@ mod plan_pc; mod source_resolution; #[cfg(test)] mod tests; +mod type_context; mod type_lookup; mod types; diff --git a/ghostscope-dwarf/src/analyzer/plan_pc.rs b/ghostscope-dwarf/src/analyzer/plan_pc.rs index 01240e26..2c803a81 100644 --- a/ghostscope-dwarf/src/analyzer/plan_pc.rs +++ b/ghostscope-dwarf/src/analyzer/plan_pc.rs @@ -196,7 +196,12 @@ impl DwarfAnalyzer { for segment in &path.segments { let pointer_type_name = plan.type_name.clone(); self.complete_unknown_pointer_target_type(module_path, &mut plan, &pointer_type_name); + let projected_type_id = match plan.type_id { + Some(type_id) => self.projected_type_id(type_id, segment)?, + None => None, + }; plan = plan.plan_access_path(&VariableAccessPath::new(vec![segment.clone()]))?; + plan.type_id = projected_type_id; if matches!(segment, VariableAccessSegment::Dereference) { self.complete_unknown_pointer_target_type( module_path, diff --git a/ghostscope-dwarf/src/analyzer/type_context.rs b/ghostscope-dwarf/src/analyzer/type_context.rs new file mode 100644 index 00000000..9a48964e --- /dev/null +++ b/ghostscope-dwarf/src/analyzer/type_context.rs @@ -0,0 +1,84 @@ +use super::DwarfAnalyzer; +use crate::{ + CompilationUnitMetadata, CuId, ModuleId, PcContext, Result, SemanticType, TypeId, TypeOrigin, + VariableAccessSegment, VariableReadPlan, +}; + +impl DwarfAnalyzer { + /// Return language and producer metadata for a loaded compilation unit. + pub fn compilation_unit_metadata( + &self, + module: ModuleId, + cu: CuId, + ) -> Result> { + let module_path = self + .module_path_for_id(module) + .ok_or_else(|| anyhow::anyhow!("Semantic module id {module:?} is not loaded"))?; + self.modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? + .compilation_unit_metadata(module, cu) + } + + /// Return language and producer metadata for the CU containing a PC context. + pub fn compilation_unit_metadata_for_context( + &self, + context: &PcContext, + ) -> Result> { + match context.cu { + Some(cu) => self.compilation_unit_metadata(context.module, cu), + None => Ok(None), + } + } + + /// Resolve the compilation-unit origin for a stable type identity. + pub fn type_origin(&self, type_id: TypeId) -> Result> { + if type_id.module != type_id.die.module || type_id.cu != type_id.die.cu { + return Err(anyhow::anyhow!("inconsistent TypeId identity: {type_id:?}")); + } + self.compilation_unit_metadata(type_id.module, type_id.cu) + .map(|metadata| metadata.map(TypeOrigin::from)) + } + + /// Combine the plan's protocol-compatible type summary with its DWARF origin. + pub fn semantic_type_for_plan(&self, plan: &VariableReadPlan) -> Result> { + let Some(summary) = plan.dwarf_type.clone() else { + return Ok(None); + }; + let origin = match plan.type_id { + Some(type_id) => self.type_origin(type_id)?, + None => None, + }; + Ok(Some(SemanticType::new(summary, plan.type_id, origin))) + } + + /// Plan constant pointer arithmetic while preserving the projected type identity. + pub fn plan_pointer_element_index( + &self, + plan: &VariableReadPlan, + index: i64, + ) -> Result { + let segment = VariableAccessSegment::ArrayIndex(index); + let projected_type_id = match plan.type_id { + Some(type_id) => self.projected_type_id(type_id, &segment)?, + None => None, + }; + let mut projected = plan.plan_pointer_element_index(index)?; + projected.type_id = projected_type_id; + Ok(projected) + } + + pub(super) fn projected_type_id( + &self, + current: TypeId, + segment: &VariableAccessSegment, + ) -> Result> { + let module_path = self.module_path_for_id(current.module).ok_or_else(|| { + anyhow::anyhow!("Semantic module id {:?} is not loaded", current.module) + })?; + self.modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? + .projected_type_id(current, segment) + } +} diff --git a/ghostscope-dwarf/src/lib.rs b/ghostscope-dwarf/src/lib.rs index 9536c879..ed289c83 100644 --- a/ghostscope-dwarf/src/lib.rs +++ b/ghostscope-dwarf/src/lib.rs @@ -44,9 +44,10 @@ pub use semantics::{ is_c_pointer_or_array_type, is_c_signed_integer_type, member_layout, strip_type_aliases, usual_c_arithmetic_comparison_plan, AddressOrigin, AddressSpaceInfo, CIntegerComparisonPlan, CIntegerComparisonType, CfaRulePlan, CompactUnwindRow, CompactUnwindStats, CompactUnwindTable, - FunctionParameter, IndexableElementLayout, InlineFrame, LvalueAddressPlan, MemberLayout, - PcContext, PcLineInfo, PcRange, PlannedAddress, PlannedAddressKind, PlannedValue, - RegisterRecoveryPlan, RuntimeComputedExpr, RuntimeComputedKind, TypeLayoutError, + CompilationUnitMetadata, FunctionParameter, IndexableElementLayout, InlineFrame, + LvalueAddressPlan, MemberLayout, PcContext, PcLineInfo, PcRange, PlannedAddress, + PlannedAddressKind, PlannedValue, ProducerInfo, RegisterRecoveryPlan, RuntimeComputedExpr, + RuntimeComputedKind, SemanticType, SourceLanguage, TypeLayoutError, TypeOrigin, UnwindDiagnostic, UnwindDiagnosticKind, VariableAccessPath, VariableAccessSegment, VariableLoweringKind, VariableLoweringPlan, VariableMaterialization, VariableMaterializationPlan, VariablePlan, VariableQueryDiagnostic, VariableReadPlan, diff --git a/ghostscope-dwarf/src/objfile/mod.rs b/ghostscope-dwarf/src/objfile/mod.rs index c257d28a..ee383a9b 100644 --- a/ghostscope-dwarf/src/objfile/mod.rs +++ b/ghostscope-dwarf/src/objfile/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod globals; pub(crate) mod loaded; pub(crate) mod loading; pub(crate) mod source_location; +pub(crate) mod type_context; pub(crate) mod unwind; pub(crate) mod variables; diff --git a/ghostscope-dwarf/src/objfile/type_context.rs b/ghostscope-dwarf/src/objfile/type_context.rs new file mode 100644 index 00000000..cc0076ba --- /dev/null +++ b/ghostscope-dwarf/src/objfile/type_context.rs @@ -0,0 +1,575 @@ +//! DWARF-backed type identity and compilation-unit metadata queries. + +use super::{variables::complete_aggregate_declaration_entry, LoadedObjfile}; +use crate::{ + core::Result, + semantics::{ + resolve_type_ref_with_origins, CompilationUnitMetadata, ProducerInfo, SourceLanguage, + TypeLoc, VariableAccessSegment, + }, + CuId, ModuleId, TypeId, +}; +use gimli::Reader; +use std::collections::HashSet; + +const MAX_TYPE_REFERENCE_DEPTH: usize = 64; + +fn debug_info_offset(cu: CuId) -> gimli::DebugInfoOffset { + gimli::DebugInfoOffset(cu.0 as usize) +} + +fn type_loc(type_id: TypeId) -> Result { + if type_id.cu != type_id.die.cu || type_id.module != type_id.die.module { + return Err(anyhow::anyhow!("inconsistent TypeId identity: {type_id:?}")); + } + + Ok(TypeLoc { + cu_off: debug_info_offset(type_id.cu), + die_off: gimli::UnitOffset( + usize::try_from(type_id.die.offset).map_err(|_| { + anyhow::anyhow!("type DIE offset does not fit this host: {type_id:?}") + })?, + ), + }) +} + +fn type_id_from_loc(module: ModuleId, loc: TypeLoc) -> TypeId { + let cu = CuId(loc.cu_off.0 as u32); + let die = crate::DieRef { + module, + cu, + offset: loc.die_off.0 as u64, + }; + TypeId { module, cu, die } +} + +fn compilation_unit_metadata_from_dwarf( + dwarf: &gimli::Dwarf, + module: ModuleId, + cu: CuId, +) -> Result> { + let header = dwarf.unit_header(debug_info_offset(cu))?; + let dwarf_version = header.version(); + let unit = dwarf.unit(header)?; + let mut entries = unit.entries(); + let Some(root) = entries.next_dfs()? else { + return Ok(None); + }; + + let dwarf_language = match root.attr_value(gimli::DW_AT_language) { + Some(gimli::AttributeValue::Language(language)) => Some(language), + _ => None, + }; + let producer = match root.attr(gimli::DW_AT_producer) { + Some(attribute) => { + let value = dwarf.attr_string(&unit, attribute.value())?; + Some(ProducerInfo::new(value.to_string_lossy()?.into_owned())) + } + None => None, + }; + + Ok(Some(CompilationUnitMetadata { + module, + cu, + language: SourceLanguage::from_dwarf(dwarf_language), + producer, + dwarf_version, + })) +} + +fn normalize_type_loc( + dwarf: &gimli::Dwarf, + type_name_index: &crate::index::TypeNameIndex, + mut loc: TypeLoc, +) -> Result> { + let mut visited = HashSet::new(); + for _ in 0..MAX_TYPE_REFERENCE_DEPTH { + if !visited.insert((loc.cu_off.0, loc.die_off.0)) { + return Err(anyhow::anyhow!( + "cycle while resolving type DIE at {:?}:{:?}", + loc.cu_off, + loc.die_off + )); + } + + let header = dwarf.unit_header(loc.cu_off)?; + let unit = dwarf.unit(header)?; + let entry = unit.entry(loc.die_off)?; + match entry.tag() { + gimli::DW_TAG_typedef + | gimli::DW_TAG_const_type + | gimli::DW_TAG_volatile_type + | gimli::DW_TAG_restrict_type => { + let Some(next) = resolve_type_ref_with_origins(dwarf, &entry, &unit)? else { + return Ok(None); + }; + loc = next; + } + gimli::DW_TAG_structure_type + | gimli::DW_TAG_class_type + | gimli::DW_TAG_union_type + | gimli::DW_TAG_enumeration_type => { + if let Some((cu_off, die_off)) = + complete_aggregate_declaration_entry(dwarf, type_name_index, &unit, &entry) + { + loc = TypeLoc { cu_off, die_off }; + } else { + return Ok(Some(loc)); + } + } + _ => return Ok(Some(loc)), + } + } + + Err(anyhow::anyhow!( + "type reference depth exceeds {MAX_TYPE_REFERENCE_DEPTH}" + )) +} + +fn pointer_like(tag: gimli::DwTag) -> bool { + matches!( + tag, + gimli::DW_TAG_pointer_type + | gimli::DW_TAG_reference_type + | gimli::DW_TAG_rvalue_reference_type + ) +} + +fn projected_member_type_loc( + dwarf: &gimli::Dwarf, + type_name_index: &crate::index::TypeNameIndex, + loc: TypeLoc, + field: &str, +) -> Result> { + let mut aggregate_loc = match normalize_type_loc(dwarf, type_name_index, loc)? { + Some(loc) => loc, + None => return Ok(None), + }; + + let header = dwarf.unit_header(aggregate_loc.cu_off)?; + let unit = dwarf.unit(header)?; + let entry = unit.entry(aggregate_loc.die_off)?; + if pointer_like(entry.tag()) { + let Some(target) = resolve_type_ref_with_origins(dwarf, &entry, &unit)? else { + return Ok(None); + }; + aggregate_loc = match normalize_type_loc(dwarf, type_name_index, target)? { + Some(loc) => loc, + None => return Ok(None), + }; + } + + let header = dwarf.unit_header(aggregate_loc.cu_off)?; + let unit = dwarf.unit(header)?; + let entry = unit.entry(aggregate_loc.die_off)?; + if !matches!( + entry.tag(), + gimli::DW_TAG_structure_type | gimli::DW_TAG_class_type | gimli::DW_TAG_union_type + ) { + return Ok(None); + } + + let mut tree = unit.entries_tree(Some(entry.offset()))?; + let root = tree.root()?; + let mut children = root.children(); + while let Some(child) = children.next()? { + let member = child.entry(); + if member.tag() != gimli::DW_TAG_member { + continue; + } + let Some(name_attribute) = member.attr(gimli::DW_AT_name) else { + continue; + }; + let name_reader = dwarf.attr_string(&unit, name_attribute.value())?; + let name = name_reader.to_string_lossy()?; + if name.as_ref() != field { + continue; + } + return resolve_type_ref_with_origins(dwarf, member, &unit); + } + + Ok(None) +} + +fn projected_element_type_loc( + dwarf: &gimli::Dwarf, + type_name_index: &crate::index::TypeNameIndex, + loc: TypeLoc, + dereference_only: bool, +) -> Result> { + let Some(base) = normalize_type_loc(dwarf, type_name_index, loc)? else { + return Ok(None); + }; + let header = dwarf.unit_header(base.cu_off)?; + let unit = dwarf.unit(header)?; + let entry = unit.entry(base.die_off)?; + let supported = if dereference_only { + pointer_like(entry.tag()) + } else { + pointer_like(entry.tag()) || entry.tag() == gimli::DW_TAG_array_type + }; + if !supported { + return Ok(None); + } + resolve_type_ref_with_origins(dwarf, &entry, &unit) +} + +fn projected_type_loc( + dwarf: &gimli::Dwarf, + type_name_index: &crate::index::TypeNameIndex, + loc: TypeLoc, + segment: &VariableAccessSegment, +) -> Result> { + match segment { + VariableAccessSegment::Field(field) => { + projected_member_type_loc(dwarf, type_name_index, loc, field) + } + VariableAccessSegment::ArrayIndex(_) => { + projected_element_type_loc(dwarf, type_name_index, loc, false) + } + VariableAccessSegment::Dereference => { + projected_element_type_loc(dwarf, type_name_index, loc, true) + } + } +} + +impl LoadedObjfile { + pub(crate) fn compilation_unit_metadata( + &self, + module: ModuleId, + cu: CuId, + ) -> Result> { + compilation_unit_metadata_from_dwarf(self.dwarf(), module, cu) + } + + pub(crate) fn projected_type_id( + &self, + current: TypeId, + segment: &VariableAccessSegment, + ) -> Result> { + projected_type_loc( + self.dwarf(), + &self.type_name_index, + type_loc(current)?, + segment, + ) + .map(|loc| loc.map(|loc| type_id_from_loc(current.module, loc))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::binary::{dwarf_reader_from_arc, DwarfReader}; + use crate::index::TypeNameIndex; + use gimli::write::{ + AttributeValue as WriteAttributeValue, DebugInfoRef as WriteDebugInfoRef, + Dwarf as WriteDwarf, EndianVec, LineProgram, Sections, Unit, + }; + use gimli::{Format, LittleEndian}; + use std::sync::Arc; + + struct Fixture { + dwarf: gimli::Dwarf, + cu: CuId, + pair: TypeLoc, + int: TypeLoc, + pair_pointer: TypeLoc, + int_array: TypeLoc, + } + + fn build_fixture() -> Fixture { + let encoding = gimli::Encoding { + format: Format::Dwarf32, + version: 5, + address_size: 8, + }; + let mut dwarf = WriteDwarf::new(); + let unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none())); + { + let unit = dwarf.units.get_mut(unit_id); + let root = unit.root(); + unit.get_mut(root).set( + gimli::DW_AT_language, + WriteAttributeValue::Language(gimli::DW_LANG_Rust), + ); + unit.get_mut(root).set( + gimli::DW_AT_producer, + WriteAttributeValue::String(b"rustc version 1.88.0".to_vec()), + ); + + let int = unit.add(root, gimli::DW_TAG_base_type); + unit.get_mut(int).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"i32".to_vec()), + ); + unit.get_mut(int) + .set(gimli::DW_AT_byte_size, WriteAttributeValue::Data1(4)); + unit.get_mut(int).set( + gimli::DW_AT_encoding, + WriteAttributeValue::Encoding(gimli::DW_ATE_signed), + ); + + let pair = unit.add(root, gimli::DW_TAG_structure_type); + unit.get_mut(pair).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"Pair".to_vec()), + ); + unit.get_mut(pair) + .set(gimli::DW_AT_byte_size, WriteAttributeValue::Data1(4)); + let member = unit.add(pair, gimli::DW_TAG_member); + unit.get_mut(member).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"value".to_vec()), + ); + unit.get_mut(member) + .set(gimli::DW_AT_type, WriteAttributeValue::UnitRef(int)); + + let pair_pointer = unit.add(root, gimli::DW_TAG_pointer_type); + unit.get_mut(pair_pointer) + .set(gimli::DW_AT_type, WriteAttributeValue::UnitRef(pair)); + unit.get_mut(pair_pointer) + .set(gimli::DW_AT_byte_size, WriteAttributeValue::Data1(8)); + + let int_array = unit.add(root, gimli::DW_TAG_array_type); + unit.get_mut(int_array) + .set(gimli::DW_AT_type, WriteAttributeValue::UnitRef(int)); + } + + let mut sections = Sections::new(EndianVec::new(LittleEndian)); + dwarf.write(&mut sections).unwrap(); + let sections = gimli::DwarfSections::load(|id| { + Ok::<_, gimli::Error>( + sections + .get(id) + .map(|section| section.slice().to_vec()) + .unwrap_or_default(), + ) + }) + .unwrap(); + let dwarf = + sections.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice()))); + + let header = dwarf.units().next().unwrap().unwrap(); + let cu_off = header.debug_info_offset().unwrap(); + let unit = dwarf.unit(header).unwrap(); + let mut pair = None; + let mut int = None; + let mut pair_pointer = None; + let mut int_array = None; + let mut entries = unit.entries(); + while let Some(entry) = entries.next_dfs().unwrap() { + let loc = TypeLoc { + cu_off, + die_off: entry.offset(), + }; + match entry.tag() { + gimli::DW_TAG_base_type => int = Some(loc), + gimli::DW_TAG_structure_type => pair = Some(loc), + gimli::DW_TAG_pointer_type => pair_pointer = Some(loc), + gimli::DW_TAG_array_type => int_array = Some(loc), + _ => {} + } + } + + Fixture { + dwarf, + cu: CuId(cu_off.0 as u32), + pair: pair.unwrap(), + int: int.unwrap(), + pair_pointer: pair_pointer.unwrap(), + int_array: int_array.unwrap(), + } + } + + fn build_cross_cu_fixture() -> (gimli::Dwarf, TypeLoc, TypeLoc) { + let encoding = gimli::Encoding { + format: Format::Dwarf32, + version: 5, + address_size: 8, + }; + let mut dwarf = WriteDwarf::new(); + let rust_unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none())); + let c_unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none())); + + let c_int = { + let c_unit = dwarf.units.get_mut(c_unit_id); + let root = c_unit.root(); + c_unit.get_mut(root).set( + gimli::DW_AT_language, + WriteAttributeValue::Language(gimli::DW_LANG_C11), + ); + c_unit.get_mut(root).set( + gimli::DW_AT_producer, + WriteAttributeValue::String(b"clang version 18".to_vec()), + ); + let int = c_unit.add(root, gimli::DW_TAG_base_type); + c_unit.get_mut(int).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"int".to_vec()), + ); + c_unit + .get_mut(int) + .set(gimli::DW_AT_byte_size, WriteAttributeValue::Data1(4)); + int + }; + + { + let rust_unit = dwarf.units.get_mut(rust_unit_id); + let root = rust_unit.root(); + rust_unit.get_mut(root).set( + gimli::DW_AT_language, + WriteAttributeValue::Language(gimli::DW_LANG_Rust), + ); + let wrapper = rust_unit.add(root, gimli::DW_TAG_structure_type); + rust_unit.get_mut(wrapper).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"ForeignValue".to_vec()), + ); + let member = rust_unit.add(wrapper, gimli::DW_TAG_member); + rust_unit.get_mut(member).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"value".to_vec()), + ); + rust_unit.get_mut(member).set( + gimli::DW_AT_type, + WriteAttributeValue::DebugInfoRef(WriteDebugInfoRef::Entry(c_unit_id, c_int)), + ); + } + + let mut sections = Sections::new(EndianVec::new(LittleEndian)); + dwarf.write(&mut sections).unwrap(); + let sections = gimli::DwarfSections::load(|id| { + Ok::<_, gimli::Error>( + sections + .get(id) + .map(|section| section.slice().to_vec()) + .unwrap_or_default(), + ) + }) + .unwrap(); + let dwarf = + sections.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice()))); + + let mut units = dwarf.units(); + let rust_header = units.next().unwrap().unwrap(); + let c_header = units.next().unwrap().unwrap(); + let rust_cu = rust_header.debug_info_offset().unwrap(); + let c_cu = c_header.debug_info_offset().unwrap(); + let rust_unit = dwarf.unit(rust_header).unwrap(); + let c_unit = dwarf.unit(c_header).unwrap(); + let find_tag = |unit: &gimli::Unit, tag| { + let mut entries = unit.entries(); + while let Some(entry) = entries.next_dfs().unwrap() { + if entry.tag() == tag { + return entry.offset(); + } + } + panic!("missing expected type tag {tag:?}"); + }; + let wrapper = find_tag(&rust_unit, gimli::DW_TAG_structure_type); + let int = find_tag(&c_unit, gimli::DW_TAG_base_type); + + ( + dwarf, + TypeLoc { + cu_off: rust_cu, + die_off: wrapper, + }, + TypeLoc { + cu_off: c_cu, + die_off: int, + }, + ) + } + + #[test] + fn reads_language_and_producer_from_compilation_unit() { + let fixture = build_fixture(); + let metadata = + compilation_unit_metadata_from_dwarf(&fixture.dwarf, ModuleId(7), fixture.cu) + .unwrap() + .unwrap(); + + assert_eq!(metadata.module, ModuleId(7)); + assert_eq!(metadata.language, SourceLanguage::Rust); + assert_eq!(metadata.dwarf_version, 5); + assert_eq!( + metadata + .producer + .as_ref() + .map(|producer| producer.raw.as_str()), + Some("rustc version 1.88.0") + ); + } + + #[test] + fn follows_member_pointer_and_array_type_references() { + let fixture = build_fixture(); + let types = TypeNameIndex::default(); + + let member = projected_type_loc( + &fixture.dwarf, + &types, + fixture.pair, + &VariableAccessSegment::Field("value".to_string()), + ) + .unwrap(); + let pointer_member = projected_type_loc( + &fixture.dwarf, + &types, + fixture.pair_pointer, + &VariableAccessSegment::Field("value".to_string()), + ) + .unwrap(); + let dereferenced = projected_type_loc( + &fixture.dwarf, + &types, + fixture.pair_pointer, + &VariableAccessSegment::Dereference, + ) + .unwrap(); + let element = projected_type_loc( + &fixture.dwarf, + &types, + fixture.int_array, + &VariableAccessSegment::ArrayIndex(3), + ) + .unwrap(); + + assert_eq!(member, Some(fixture.int)); + assert_eq!(pointer_member, Some(fixture.int)); + assert_eq!(dereferenced, Some(fixture.pair)); + assert_eq!(element, Some(fixture.int)); + } + + #[test] + fn follows_cross_cu_member_type_and_changes_language_origin() { + let (dwarf, wrapper, c_int) = build_cross_cu_fixture(); + let projected = projected_type_loc( + &dwarf, + &TypeNameIndex::default(), + wrapper, + &VariableAccessSegment::Field("value".to_string()), + ) + .unwrap() + .unwrap(); + let metadata = compilation_unit_metadata_from_dwarf( + &dwarf, + ModuleId(3), + CuId(projected.cu_off.0 as u32), + ) + .unwrap() + .unwrap(); + + assert_eq!(projected, c_int); + assert_eq!(metadata.language, SourceLanguage::C); + assert_eq!( + metadata + .producer + .as_ref() + .map(|producer| producer.raw.as_str()), + Some("clang version 18") + ); + } +} diff --git a/ghostscope-dwarf/src/objfile/variables.rs b/ghostscope-dwarf/src/objfile/variables.rs index 81c2314d..2981f5df 100644 --- a/ghostscope-dwarf/src/objfile/variables.rs +++ b/ghostscope-dwarf/src/objfile/variables.rs @@ -53,7 +53,7 @@ fn type_id( } } -fn complete_aggregate_declaration_entry( +pub(super) fn complete_aggregate_declaration_entry( dwarf: &gimli::Dwarf, type_name_index: &crate::index::TypeNameIndex, unit: &gimli::Unit, diff --git a/ghostscope-dwarf/src/semantics/mod.rs b/ghostscope-dwarf/src/semantics/mod.rs index b222df8c..d53ba0d2 100644 --- a/ghostscope-dwarf/src/semantics/mod.rs +++ b/ghostscope-dwarf/src/semantics/mod.rs @@ -1,5 +1,6 @@ pub mod c_types; pub mod pc_context; +pub mod type_context; pub mod unwind_plan; pub mod variable_plan; @@ -20,7 +21,12 @@ pub(crate) use pc::{range_contains_pc, ranges_contain_pc}; pub use pc_context::{ AddressSpaceInfo, FunctionParameter, InlineFrame, PcContext, PcLineInfo, PcRange, }; -pub(crate) use types::{resolve_type_ref_in_same_unit_with_origins, resolve_type_ref_with_origins}; +pub use type_context::{ + CompilationUnitMetadata, ProducerInfo, SemanticType, SourceLanguage, TypeOrigin, +}; +pub(crate) use types::{ + resolve_type_ref_in_same_unit_with_origins, resolve_type_ref_with_origins, TypeLoc, +}; pub use unwind_plan::{ CfaRulePlan, CompactUnwindRow, CompactUnwindStats, CompactUnwindTable, RegisterRecoveryPlan, UnwindDiagnostic, UnwindDiagnosticKind, diff --git a/ghostscope-dwarf/src/semantics/type_context.rs b/ghostscope-dwarf/src/semantics/type_context.rs new file mode 100644 index 00000000..0bf2fe33 --- /dev/null +++ b/ghostscope-dwarf/src/semantics/type_context.rs @@ -0,0 +1,132 @@ +//! Source-language and producer context attached to DWARF-backed types. + +use crate::core::{CuId, ModuleId, TypeId}; +use crate::TypeInfo; + +/// Normalized source-language family for semantic dispatch. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SourceLanguage { + C, + Cpp, + Rust, + Other(u16), + Unknown, +} + +impl SourceLanguage { + pub fn from_dwarf(language: Option) -> Self { + match language { + Some( + gimli::DW_LANG_C89 + | gimli::DW_LANG_C + | gimli::DW_LANG_C99 + | gimli::DW_LANG_C11 + | gimli::DW_LANG_C17, + ) => Self::C, + Some( + gimli::DW_LANG_C_plus_plus + | gimli::DW_LANG_C_plus_plus_03 + | gimli::DW_LANG_C_plus_plus_11 + | gimli::DW_LANG_C_plus_plus_14 + | gimli::DW_LANG_C_plus_plus_17 + | gimli::DW_LANG_C_plus_plus_20, + ) => Self::Cpp, + Some(gimli::DW_LANG_Rust) => Self::Rust, + Some(language) => Self::Other(language.0), + None => Self::Unknown, + } + } +} + +/// Raw compiler producer description from `DW_AT_producer`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ProducerInfo { + pub raw: String, +} + +impl ProducerInfo { + pub fn new(raw: impl Into) -> Self { + Self { raw: raw.into() } + } +} + +/// Metadata that controls language-aware interpretation for one compilation unit. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CompilationUnitMetadata { + pub module: ModuleId, + pub cu: CuId, + pub language: SourceLanguage, + pub producer: Option, + pub dwarf_version: u16, +} + +/// Stable origin for a type DIE. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TypeOrigin { + pub module: ModuleId, + pub cu: CuId, + pub language: SourceLanguage, + pub producer: Option, + pub dwarf_version: u16, +} + +impl From for TypeOrigin { + fn from(metadata: CompilationUnitMetadata) -> Self { + Self { + module: metadata.module, + cu: metadata.cu, + language: metadata.language, + producer: metadata.producer, + dwarf_version: metadata.dwarf_version, + } + } +} + +/// A protocol-compatible type summary plus its optional DWARF identity and origin. +#[derive(Debug, Clone, PartialEq)] +pub struct SemanticType { + pub summary: TypeInfo, + pub id: Option, + pub origin: Option, +} + +impl SemanticType { + pub fn new(summary: TypeInfo, id: Option, origin: Option) -> Self { + Self { + summary, + id, + origin, + } + } +} + +#[cfg(test)] +mod tests { + use super::SourceLanguage; + + #[test] + fn normalizes_supported_language_families() { + assert_eq!( + SourceLanguage::from_dwarf(Some(gimli::DW_LANG_C17)), + SourceLanguage::C + ); + assert_eq!( + SourceLanguage::from_dwarf(Some(gimli::DW_LANG_C_plus_plus_20)), + SourceLanguage::Cpp + ); + assert_eq!( + SourceLanguage::from_dwarf(Some(gimli::DW_LANG_Rust)), + SourceLanguage::Rust + ); + } + + #[test] + fn preserves_unknown_language_codes() { + let custom = gimli::DwLang(0x8abc); + assert_eq!( + SourceLanguage::from_dwarf(Some(custom)), + SourceLanguage::Other(0x8abc) + ); + assert_eq!(SourceLanguage::from_dwarf(None), SourceLanguage::Unknown); + } +} From 96b56345d21655e14f5c6ee5ec8253cc457aab73 Mon Sep 17 00:00:00 2001 From: swananan Date: Sat, 11 Jul 2026 14:06:52 +0800 Subject: [PATCH 2/2] feat: support Rust tuple field projections Preserve numeric tuple access in the compiler AST and dispatch it by type origin in ghostscope-dwarf. Map Rust indices to rustc __N fields without leaking producer layout into eBPF lowering. Propagate TypeId through static and dynamic access plans, including array elements, and cover direct, dynamic-index, and cast-based tuple reads. --- docs/scripting.md | 8 +- docs/zh/scripting.md | 8 +- .../fixtures/rust_global_program/src/main.rs | 2 + e2e-tests/tests/rust_script_execution.rs | 109 +++++++++++++ ghostscope-compiler/src/ebpf/codegen/args.rs | 5 +- ghostscope-compiler/src/ebpf/dwarf_bridge.rs | 44 ++++++ .../src/ebpf/expression/casts.rs | 11 +- .../src/ebpf/expression/dwarf_access.rs | 148 ++++++++++++++++-- .../src/ebpf/expression/mod.rs | 6 +- ghostscope-compiler/src/script/ast.rs | 6 +- ghostscope-compiler/src/script/grammar.pest | 3 +- ghostscope-compiler/src/script/parser/expr.rs | 22 ++- .../src/script/parser/tests.rs | 29 ++++ ghostscope-dwarf/src/analyzer/plan_global.rs | 4 + ghostscope-dwarf/src/analyzer/plan_pc.rs | 5 +- ghostscope-dwarf/src/analyzer/type_context.rs | 86 +++++++++- ghostscope-dwarf/src/language/mod.rs | 62 ++++++++ ghostscope-dwarf/src/language/rust.rs | 5 + ghostscope-dwarf/src/lib.rs | 1 + ghostscope-dwarf/src/objfile/type_context.rs | 18 +++ ghostscope-dwarf/src/objfile/variables.rs | 61 +++++--- .../src/semantics/variable_plan/mod.rs | 54 ++++++- .../src/semantics/variable_plan/tests.rs | 41 +++++ 23 files changed, 682 insertions(+), 56 deletions(-) create mode 100644 ghostscope-dwarf/src/language/mod.rs create mode 100644 ghostscope-dwarf/src/language/rust.rs diff --git a/docs/scripting.md b/docs/scripting.md index c4ded7d9..e21ab79f 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -120,12 +120,12 @@ GhostScope's script syntax is source-language agnostic, but real-world support d | --- | --- | --- | | C | Best | This is the primary target. Plain locals, globals, x86_64 executable static thread-local variables, pointers, arrays, structs, enums, and C strings map most directly to the current DWARF readers and script operators. | | C++ | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Beyond name resolution, most C++-specific language features are not modeled yet, so the best results come from simple, C-like layouts and scalar fields. | -| Rust | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Beyond name resolution, most Rust-specific language features are not modeled yet, so the best results come from plain globals, scalar fields, and straightforward struct layouts. | +| Rust | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Numeric tuple fields such as `value.0` are supported for tuples and tuple structs when Rust DWARF type identity is available. Most other Rust-specific features are not modeled yet. | Practical guidance: - Prefer C targets when you need the highest success rate for complex DWARF expressions. - GhostScope recognizes `DW_OP_form_tls_address`, which is used for both static and dynamic TLS. Runtime address resolution currently supports only x86_64 executable static TLS; dynamic/shared-library TLS is not modeled yet. -- For C++ and Rust, think of GhostScope as "DWARF layout aware" rather than "language semantics aware". +- C++ remains primarily DWARF-layout aware. Rust tuple fields are the first language-aware projection, but broader Rust semantics are not modeled yet. - In C++ and Rust, start from demangled function/global names, then probe simple fields first. If name lookup is ambiguous, fall back to line- or address-based trace patterns. ## Variables @@ -215,6 +215,10 @@ print x; print person.name; print config.settings.timeout; +// Rust tuple and tuple-struct fields +print GLOBAL_TUPLE.0; +print GLOBAL_PAIR.1; + // Array access (literal and expression indices) print arr[0]; print arr[1]; diff --git a/docs/zh/scripting.md b/docs/zh/scripting.md index f4cf15c8..34ab3b26 100644 --- a/docs/zh/scripting.md +++ b/docs/zh/scripting.md @@ -114,12 +114,12 @@ GhostScope 的脚本语法本身与源语言无关,但实际效果取决于被 | --- | --- | --- | | C | 最好 | 这是当前的主要优化目标。局部变量、全局变量、x86_64 可执行文件中的 static 线程局部变量、指针、数组、结构体、枚举和 C 字符串这类 C 风格布局,与现有 DWARF 读取和脚本运算最匹配。 | | C++ | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。除名字解析外,C++ 语言特性目前大多还没有建模,实际更接近“按 DWARF 布局访问”,适合简单、接近 C 的数据布局和标量字段。 | -| Rust | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。除名字解析外,Rust 语言特性目前大多还没有建模,实际更接近“按 DWARF 布局访问”,适合简单全局、标量字段和较直白的结构体布局。 | +| Rust | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。当 Rust DWARF 类型身份可用时,tuple 和 tuple struct 支持 `value.0` 这类数字成员访问;其他大多数 Rust 语言特性仍未建模。 | 实践建议: - 需要较高成功率的复杂 DWARF 表达式时,优先选择 C 目标。 - GhostScope 能识别 `DW_OP_form_tls_address`,这个操作既会用于 static TLS,也会用于 dynamic TLS。运行时地址解析目前只支持 x86_64 可执行文件 static TLS;dynamic/shared-library TLS 尚未建模。 -- 对 C++ 和 Rust,建议把 GhostScope 理解为“理解 DWARF 布局”,而不是“理解语言语义”。 +- C++ 目前仍主要按 DWARF 布局访问。Rust tuple 成员是第一个语言语义投影,但更广泛的 Rust 语义仍未建模。 - 在 C++ 和 Rust 上,先从 demangle 后的函数名/全局名入手,再逐步验证简单字段;若名字解析有歧义,优先退回到按源码行号或地址下探。 ## 变量 @@ -211,6 +211,10 @@ print x; print person.name; print config.settings.timeout; +// Rust tuple 与 tuple struct 成员 +print GLOBAL_TUPLE.0; +print GLOBAL_PAIR.1; + // 数组访问(支持字面量和表达式下标) print arr[0]; print arr[1]; diff --git a/e2e-tests/tests/fixtures/rust_global_program/src/main.rs b/e2e-tests/tests/fixtures/rust_global_program/src/main.rs index 2e64c0b7..de9ca807 100644 --- a/e2e-tests/tests/fixtures/rust_global_program/src/main.rs +++ b/e2e-tests/tests/fixtures/rust_global_program/src/main.rs @@ -91,6 +91,7 @@ static DATA_STRINGS: [&[u8]; 2] = [DATA_ALPHA, DATA_OMEGA]; pub static mut GLOBAL_TUPLE: (i32, bool) = (1, true); pub static mut GLOBAL_PAIR: Pair = Pair(2, 3); +pub static GLOBAL_PAIRS: [Pair; 2] = [Pair(5, 8), Pair(13, 21)]; pub static mut GLOBAL_UNION: NumberUnion = NumberUnion { int_value: 10 }; pub static mut GLOBAL_SLICE: &'static [u8] = DATA_ALPHA; pub static mut GLOBAL_NICHE: Option = NonZeroU32::new(7); @@ -169,6 +170,7 @@ fn touch_globals() -> i32 { let total = CONFIG.a as i64 + G_MESSAGE.len() as i64 + + GLOBAL_PAIRS[0].0 as i64 + union_value as i64 + pinned_value as i64 + GLOBAL_PHANTOM.value as i64 diff --git a/e2e-tests/tests/rust_script_execution.rs b/e2e-tests/tests/rust_script_execution.rs index 58e4351e..27ba2088 100644 --- a/e2e-tests/tests/rust_script_execution.rs +++ b/e2e-tests/tests/rust_script_execution.rs @@ -31,6 +31,115 @@ async fn spawn_rust_global_program() -> anyhow::Result anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let tuple_path = ghostscope_dwarf::VariableAccessPath::new(vec![ + ghostscope_dwarf::VariableAccessSegment::TupleIndex(0), + ]); + let (_, plan) = analyzer + .plan_global_access_read_plan(&binary_path, "GLOBAL_TUPLE", &tuple_path)? + .ok_or_else(|| anyhow::anyhow!("expected GLOBAL_TUPLE.0 read plan"))?; + + assert_eq!(plan.name, "GLOBAL_TUPLE.0"); + assert_eq!(plan.access_path, tuple_path); + assert!(plan.type_id.is_some(), "plan: {plan:?}"); + let semantic_type = analyzer + .semantic_type_for_plan(&plan)? + .ok_or_else(|| anyhow::anyhow!("expected semantic type for GLOBAL_TUPLE.0"))?; + assert_eq!(semantic_type.id, plan.type_id); + assert_eq!( + semantic_type.origin.map(|origin| origin.language), + Some(ghostscope_dwarf::SourceLanguage::Rust) + ); + + let (_, pair_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "GLOBAL_PAIR", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected GLOBAL_PAIR read plan"))?; + let pair_type = pair_plan + .dwarf_type + .as_ref() + .ok_or_else(|| anyhow::anyhow!("expected GLOBAL_PAIR type"))?; + let layout = analyzer.tuple_member_layout_in_module(&binary_path, pair_type, 1)?; + assert_eq!(layout.offset, 4); + assert_eq!(layout.member_type.type_name(), "i32"); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_tuple_fields() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + let pair_index = 1; + print "RTUP:{}:{}", GLOBAL_TUPLE.0, GLOBAL_TUPLE.1; + print "RPAIR:{}:{}", GLOBAL_PAIR.0, GLOBAL_PAIR.1; + print "DPAIR:{}", GLOBAL_PAIRS[pair_index].0; + print "CPAIR:{}", cast(&GLOBAL_PAIR, "Pair *").0; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout.lines().any(|line| { + line.contains("RTUP:") && (line.contains(":true") || line.contains(":false")) + }), + "Expected typed tuple output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + let Some((_, payload)) = line.split_once("RPAIR:") else { + return false; + }; + let mut fields = payload.split(':'); + let Some(left) = fields.next() else { + return false; + }; + let Some(right) = fields.next() else { + return false; + }; + left.trim().parse::().is_ok() + && right + .split_whitespace() + .next() + .is_some_and(|value| value.parse::().is_ok()) + }), + "Expected typed tuple struct output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + assert!( + stdout.contains("DPAIR:13"), + "Expected dynamic-index tuple output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + line.split_once("CPAIR:") + .and_then(|(_, value)| value.split_whitespace().next()) + .is_some_and(|value| value.parse::().is_ok()) + }), + "Expected cast tuple output: {stdout}" + ); + + Ok(()) +} + #[tokio::test] async fn test_rust_script_print_globals() -> anyhow::Result<()> { init(); diff --git a/ghostscope-compiler/src/ebpf/codegen/args.rs b/ghostscope-compiler/src/ebpf/codegen/args.rs index 7f7a2fdd..be03acdb 100644 --- a/ghostscope-compiler/src/ebpf/codegen/args.rs +++ b/ghostscope-compiler/src/ebpf/codegen/args.rs @@ -415,6 +415,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { // 5) Complex lvalue shapes -> DWARF runtime read expr @ (E::MemberAccess(_, _) + | E::TupleAccess(_, _) | E::ArrayAccess(_, _) | E::PointerDeref(_) | E::ChainAccess(_)) => { @@ -793,6 +794,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { match e { E::Variable(s) => s.clone(), E::MemberAccess(obj, field) => format!("{}.{field}", inner(obj)), + E::TupleAccess(obj, index) => format!("{}.{index}", inner(obj)), E::ArrayAccess(arr, idx) => format!("{}[{}]", inner(arr), inner(idx)), E::PointerDeref(p) => format!("*{}", inner(p)), E::AddressOf(p) => format!("&{}", inner(p)), @@ -864,7 +866,8 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { | E::UnaryBitNot(inner) | E::PointerDeref(inner) | E::AddressOf(inner) - | E::MemberAccess(inner, _) => Self::expr_contains_builtin(inner), + | E::MemberAccess(inner, _) + | E::TupleAccess(inner, _) => Self::expr_contains_builtin(inner), E::Cast { expr, .. } => Self::expr_contains_builtin(expr), E::ArrayAccess(base, index) => { Self::expr_contains_builtin(base) || Self::expr_contains_builtin(index) diff --git a/ghostscope-compiler/src/ebpf/dwarf_bridge.rs b/ghostscope-compiler/src/ebpf/dwarf_bridge.rs index 5dfadf25..7368599f 100644 --- a/ghostscope-compiler/src/ebpf/dwarf_bridge.rs +++ b/ghostscope-compiler/src/ebpf/dwarf_bridge.rs @@ -1345,6 +1345,10 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { let base = expand_aliases(ctx, obj, visited, depth + 1)?; E::MemberAccess(Box::new(base), field.clone()) } + E::TupleAccess(obj, index) => { + let base = expand_aliases(ctx, obj, visited, depth + 1)?; + E::TupleAccess(Box::new(base), *index) + } E::ArrayAccess(arr, idx) => { let base = expand_aliases(ctx, arr, visited, depth + 1)?; let idx2 = expand_aliases(ctx, idx, visited, depth + 1)?; @@ -1426,6 +1430,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { match &expanded { Expr::Variable(var_name) => self.query_dwarf_for_variable_plan(var_name), Expr::MemberAccess(_, _) + | Expr::TupleAccess(_, _) | Expr::ArrayAccess(_, _) | Expr::ChainAccess(_) | Expr::PointerDeref(_) => { @@ -1607,6 +1612,10 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { out.push('.'); out.push_str(field); } + VariableAccessSegment::TupleIndex(index) => { + out.push('.'); + out.push_str(&index.to_string()); + } VariableAccessSegment::ArrayIndex(index) => { out.push('['); out.push_str(&index.to_string()); @@ -1643,6 +1652,13 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { segments.push(VariableAccessSegment::Field(field.clone())); Ok(Some(base)) } + crate::script::Expr::TupleAccess(obj, index) => { + let Some(base) = append_segments(obj, segments)? else { + return Ok(None); + }; + segments.push(VariableAccessSegment::TupleIndex(*index)); + Ok(Some(base)) + } crate::script::Expr::ArrayAccess(array, index) => { let crate::script::Expr::Int(index) = index.as_ref() else { return Err(CodeGenError::NotImplemented( @@ -1769,6 +1785,34 @@ mod tests { ); } + #[test] + fn access_path_from_expr_preserves_tuple_indices() { + let expr = Expr::TupleAccess( + Box::new(Expr::TupleAccess( + Box::new(Expr::Variable("nested".to_string())), + 1, + )), + 0, + ); + + let (base, path) = EbpfContext::<'static, 'static>::access_path_from_expr(&expr) + .expect("access path should parse") + .expect("expression should be flattenable"); + + assert_eq!(base, "nested"); + assert_eq!( + path.segments, + vec![ + VariableAccessSegment::TupleIndex(1), + VariableAccessSegment::TupleIndex(0), + ] + ); + assert_eq!( + EbpfContext::<'static, 'static>::access_path_to_string(&base, &path), + "nested.1.0" + ); + } + #[test] fn access_path_from_expr_rejects_dynamic_array_index() { let expr = Expr::ArrayAccess( diff --git a/ghostscope-compiler/src/ebpf/expression/casts.rs b/ghostscope-compiler/src/ebpf/expression/casts.rs index 603a2bf5..0d81b82d 100644 --- a/ghostscope-compiler/src/ebpf/expression/casts.rs +++ b/ghostscope-compiler/src/ebpf/expression/casts.rs @@ -210,6 +210,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: pointee_type, module_path, + type_id: None, }, }); } @@ -220,6 +221,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: target_type, module_path, + type_id: None, }, }) } @@ -244,7 +246,8 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { match ghostscope_dwarf::strip_type_aliases(&target_type) { DwarfType::PointerType { .. } => { - let Some(element_info) = Self::indexable_info_from_type(&target_type, module_path) + let Some(element_info) = + Self::indexable_info_from_type(&target_type, module_path, None) else { return Ok(None); }; @@ -252,7 +255,8 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { Ok(Some((element_info, base_address))) } DwarfType::ArrayType { .. } => { - let Some(element_info) = Self::indexable_info_from_type(&target_type, module_path) + let Some(element_info) = + Self::indexable_info_from_type(&target_type, module_path, None) else { return Ok(None); }; @@ -266,11 +270,13 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { pub(super) fn indexable_info_from_type( dwarf_type: &DwarfType, module_path: Option, + type_id: Option, ) -> Option { ghostscope_dwarf::indexable_element_layout(dwarf_type).map(|layout| IndexableElementInfo { element_type: layout.element_type, stride: layout.stride, module_path, + type_id, }) } @@ -324,6 +330,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: element_info.element_type, module_path: element_info.module_path, + type_id: element_info.type_id, }, }) } diff --git a/ghostscope-compiler/src/ebpf/expression/dwarf_access.rs b/ghostscope-compiler/src/ebpf/expression/dwarf_access.rs index 48f32443..fa098997 100644 --- a/ghostscope-compiler/src/ebpf/expression/dwarf_access.rs +++ b/ghostscope-compiler/src/ebpf/expression/dwarf_access.rs @@ -3,7 +3,7 @@ use crate::ebpf::context::{CodeGenError, EbpfContext, Result, RuntimeAddress}; use crate::script::Expr; use ghostscope_dwarf::{ AmbiguityReason, Availability, RuntimeRequirement, TypeInfo as DwarfType, TypeLayoutError, - UnsupportedReason, VariableReadPlan, + UnsupportedReason, VariableAccessSegment, VariableReadPlan, }; use inkwell::values::{BasicValueEnum, IntValue, PointerValue}; use inkwell::AddressSpace; @@ -86,6 +86,13 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { return Ok(value); } } + if let crate::script::Expr::TupleAccess(obj_expr, index) = expr { + if let Some((value, _member_type)) = + self.compile_dynamic_tuple_access_value(obj_expr, *index)? + { + return Ok(value); + } + } if matches!(expr, crate::script::Expr::PointerDeref(_)) { if let Some(lvalue) = self.dynamic_lvalue_address_and_type(expr)? { return self @@ -166,6 +173,20 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { Ok(Some((value, member_type))) } + pub(in crate::ebpf) fn compile_dynamic_tuple_access_value( + &mut self, + obj_expr: &Expr, + index: u32, + ) -> Result, DwarfType)>> { + let tuple_expr = Expr::TupleAccess(Box::new(obj_expr.clone()), index); + let Some(member_lvalue) = self.dynamic_lvalue_address_and_type(&tuple_expr)? else { + return Ok(None); + }; + let member_type = member_lvalue.type_info.dwarf_type; + let value = self.read_dynamic_address_value(member_lvalue.address, &member_type)?; + Ok(Some((value, member_type))) + } + pub(in crate::ebpf) fn dynamic_lvalue_address_and_type( &mut self, expr: &Expr, @@ -217,6 +238,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: element_info.element_type, module_path: element_info.module_path, + type_id: element_info.type_id, }, })); } @@ -231,6 +253,10 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { }; let (member_offset, member_type) = self.dynamic_member_offset_and_type(&base_lvalue.type_info, field)?; + let member_type_id = self.project_dynamic_type_id( + base_lvalue.type_info.type_id, + &VariableAccessSegment::Field(field.clone()), + )?; let member_offset = self.context.i64_type().const_int(member_offset, false); let member_address = self .builder @@ -245,6 +271,41 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: member_type, module_path: base_lvalue.type_info.module_path, + type_id: member_type_id, + }, + })); + } + + if let Expr::TupleAccess(obj_expr, index) = expr { + let Some(object_lvalue) = self.dynamic_lvalue_address_and_type(obj_expr)? else { + return Ok(None); + }; + let Some(base_lvalue) = self.dynamic_member_base_address_and_type(object_lvalue)? + else { + return Ok(None); + }; + let module_path = base_lvalue.type_info.module_path.clone(); + let (member_offset, member_type) = + self.dynamic_tuple_offset_and_type(&base_lvalue.type_info, *index)?; + let member_type_id = self.project_dynamic_type_id( + base_lvalue.type_info.type_id, + &VariableAccessSegment::TupleIndex(*index), + )?; + let member_offset = self.context.i64_type().const_int(member_offset, false); + let member_address = self + .builder + .build_int_add( + base_lvalue.address.value, + member_offset, + "dynamic_tuple_lvalue_address", + ) + .map_err(|err| CodeGenError::Builder(err.to_string()))?; + return Ok(Some(DynamicLvalue { + address: base_lvalue.address.with_value(member_address), + type_info: DynamicTypeInfo { + dwarf_type: member_type, + module_path, + type_id: member_type_id, }, })); } @@ -257,6 +318,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { object: DynamicLvalue<'ctx>, ) -> Result>> { let module_path = object.type_info.module_path.clone(); + let type_id = object.type_info.type_id; let object_type = self.complete_dynamic_member_element_type( object.type_info.dwarf_type, module_path.as_deref(), @@ -267,6 +329,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: object_type, module_path, + type_id, }, })), DwarfType::PointerType { target_type, .. } => { @@ -299,6 +362,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: target_type, module_path, + type_id, }, })) } @@ -326,6 +390,51 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { } } + fn dynamic_tuple_offset_and_type( + &self, + aggregate: &DynamicTypeInfo, + index: u32, + ) -> Result<(u64, DwarfType)> { + let aggregate_type = self.complete_dynamic_member_element_type( + aggregate.dwarf_type.clone(), + aggregate.module_path.as_deref(), + ); + let analyzer = self + .process_analyzer + .ok_or_else(|| CodeGenError::DwarfError("No DWARF analyzer available".to_string()))?; + let fallback_module_path = self + .current_compile_time_context + .as_ref() + .map(|context| PathBuf::from(&context.module_path)); + let module_path = aggregate + .module_path + .as_deref() + .or(fallback_module_path.as_deref()) + .ok_or_else(|| { + CodeGenError::DwarfError("Tuple projection has no originating module".to_string()) + })?; + let layout = match aggregate.type_id { + Some(type_id) => analyzer.tuple_member_layout(type_id, &aggregate_type, index), + None => analyzer.tuple_member_layout_in_module(module_path, &aggregate_type, index), + } + .map_err(|error| CodeGenError::DwarfError(error.to_string()))?; + Ok((layout.offset, layout.member_type)) + } + + fn project_dynamic_type_id( + &self, + current: Option, + segment: &VariableAccessSegment, + ) -> Result> { + let Some(current) = current else { + return Ok(None); + }; + self.process_analyzer + .ok_or_else(|| CodeGenError::DwarfError("No DWARF analyzer available".to_string()))? + .project_type_id(current, segment) + .map_err(|error| CodeGenError::DwarfError(error.to_string())) + } + fn dynamic_array_base_from_plan( &mut self, array_plan: &VariableReadPlan, @@ -337,8 +446,10 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { let array_type = array_plan.dwarf_type.as_ref().ok_or_else(|| { CodeGenError::DwarfError("Array expression has no DWARF type information".to_string()) })?; - let element_info = - Self::indexable_info_from_type(array_type, module_path).ok_or_else(|| { + let element_type_id = self + .project_dynamic_type_id(array_plan.type_id, &VariableAccessSegment::ArrayIndex(0))?; + let element_info = Self::indexable_info_from_type(array_type, module_path, element_type_id) + .ok_or_else(|| { CodeGenError::TypeError(format!( "dynamic array index requires array or pointer type, got '{}'", array_type.type_name() @@ -432,9 +543,14 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { self.dynamic_lvalue_address_and_type(&expanded_array_expr)? { let module_path = array_lvalue.type_info.module_path.clone(); + let element_type_id = self.project_dynamic_type_id( + array_lvalue.type_info.type_id, + &VariableAccessSegment::ArrayIndex(0), + )?; let element_info = Self::indexable_info_from_type( &array_lvalue.type_info.dwarf_type, module_path, + element_type_id, ) .ok_or_else(|| { CodeGenError::TypeError(format!( @@ -520,6 +636,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { type_info: DynamicTypeInfo { dwarf_type: element_info.element_type, module_path: element_info.module_path, + type_id: element_info.type_id, }, })) } @@ -539,9 +656,13 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { if let Some(plan) = self.query_dwarf_for_complex_expr(&expanded)? { if let Some(dwarf_type) = plan.dwarf_type.as_ref() { - if let Some(info) = - Self::indexable_info_from_type(dwarf_type, plan.module_path.clone()) - { + let element_type_id = self + .project_dynamic_type_id(plan.type_id, &VariableAccessSegment::ArrayIndex(0))?; + if let Some(info) = Self::indexable_info_from_type( + dwarf_type, + plan.module_path.clone(), + element_type_id, + ) { return Ok(Some(info)); } } @@ -552,9 +673,15 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { { if let Some(plan) = self.query_dwarf_for_complex_expr(&base_expr)? { if let Some(dwarf_type) = plan.dwarf_type.as_ref() { - if let Some(info) = - Self::indexable_info_from_type(dwarf_type, plan.module_path.clone()) - { + let element_type_id = self.project_dynamic_type_id( + plan.type_id, + &VariableAccessSegment::ArrayIndex(0), + )?; + if let Some(info) = Self::indexable_info_from_type( + dwarf_type, + plan.module_path.clone(), + element_type_id, + ) { return Ok(Some(info)); } } @@ -768,6 +895,9 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { Expr::MemberAccess(obj, field) => { format!("{}.{}", Self::expr_to_debug_string(obj), field) } + Expr::TupleAccess(obj, index) => { + format!("{}.{}", Self::expr_to_debug_string(obj), index) + } Expr::ArrayAccess(arr, _) => format!("{}[index]", Self::expr_to_debug_string(arr)), Expr::Cast { expr, target_type } => format!( "cast({}, \"{}\")", diff --git a/ghostscope-compiler/src/ebpf/expression/mod.rs b/ghostscope-compiler/src/ebpf/expression/mod.rs index e210bc1f..fa7672d0 100644 --- a/ghostscope-compiler/src/ebpf/expression/mod.rs +++ b/ghostscope-compiler/src/ebpf/expression/mod.rs @@ -5,7 +5,7 @@ use super::context::{CodeGenError, EbpfContext, Result, RuntimeAddress}; use super::expression_plan::{BinaryEmitKind, BuiltinCallPlan}; use crate::script::Expr; -use ghostscope_dwarf::{CIntegerComparisonType, TypeInfo as DwarfType}; +use ghostscope_dwarf::{CIntegerComparisonType, TypeId, TypeInfo as DwarfType}; use inkwell::values::BasicValueEnum; use inkwell::AddressSpace; @@ -24,6 +24,7 @@ use tracing::debug; pub(super) struct DynamicTypeInfo { pub(super) dwarf_type: DwarfType, pub(super) module_path: Option, + pub(super) type_id: Option, } pub(super) struct DynamicLvalue<'ctx> { @@ -35,6 +36,7 @@ struct IndexableElementInfo { element_type: DwarfType, stride: u64, module_path: Option, + type_id: Option, } impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { @@ -479,7 +481,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { binary_plan.integer_semantics, ) } - Expr::MemberAccess(_, _) => { + Expr::MemberAccess(_, _) | Expr::TupleAccess(_, _) => { // Use unified DWARF expression compilation self.compile_dwarf_expression(expr) } diff --git a/ghostscope-compiler/src/script/ast.rs b/ghostscope-compiler/src/script/ast.rs index 1c1cd49b..b31e805d 100644 --- a/ghostscope-compiler/src/script/ast.rs +++ b/ghostscope-compiler/src/script/ast.rs @@ -8,6 +8,7 @@ pub enum Expr { UnaryBitNot(Box), Variable(String), MemberAccess(Box, String), // person.name + TupleAccess(Box, u32), // tuple.0 PointerDeref(Box), // *ptr AddressOf(Box), // &expr ArrayAccess(Box, Box), // arr[0] (new) @@ -216,8 +217,9 @@ pub fn infer_type(expr: &Expr) -> Result { // For variable references, return a default type to allow compilation to continue, actual type checking will be done in code generation phase Expr::Variable(_) => Ok(VarType::Int), // Temporarily assume variables are integer type to let parsing pass Expr::MemberAccess(_, _) => Ok(VarType::Int), // Same as above - Expr::PointerDeref(_) => Ok(VarType::Int), // Same as above - Expr::AddressOf(_) => Ok(VarType::Int), // Address as integer/pointer value for now + Expr::TupleAccess(_, _) => Ok(VarType::Int), // Resolved through language-aware DWARF planning + Expr::PointerDeref(_) => Ok(VarType::Int), // Same as above + Expr::AddressOf(_) => Ok(VarType::Int), // Address as integer/pointer value for now Expr::ArrayAccess(_, _) => Ok(VarType::Int), // New: array access returns element type (assume int for now) Expr::Cast { .. } => Ok(VarType::Int), // Cast type is resolved during codegen. Expr::ChainAccess(_) => Ok(VarType::Int), // New: chain access returns final member type (assume int for now) diff --git a/ghostscope-compiler/src/script/grammar.pest b/ghostscope-compiler/src/script/grammar.pest index 441748e1..7e2c7f45 100644 --- a/ghostscope-compiler/src/script/grammar.pest +++ b/ghostscope-compiler/src/script/grammar.pest @@ -119,7 +119,7 @@ complex_variable = { chain_access | array_access | member_access | pointer_deref postfix_access = { postfix_base ~ postfix_suffix+ } postfix_base = { cast_call | special_var | identifier | "(" ~ expr ~ ")" } postfix_suffix = { member_suffix | index_suffix } -member_suffix = { "." ~ identifier } +member_suffix = { "." ~ (tuple_index | identifier) } index_suffix = { "[" ~ expr ~ "]" } chain_access = { identifier ~ ("." ~ identifier)* ~ ("[" ~ expr ~ "]")? } @@ -134,6 +134,7 @@ add_op = { "+" | "-" } mul_op = { "*" | "/" | "%" } identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } +tuple_index = @{ ASCII_DIGIT+ } // Integer literals int = @{ ASCII_DIGIT+ } hex_int = @{ "0x" ~ ASCII_HEX_DIGIT+ } diff --git a/ghostscope-compiler/src/script/parser/expr.rs b/ghostscope-compiler/src/script/parser/expr.rs index 57581acd..61eebb4b 100644 --- a/ghostscope-compiler/src/script/parser/expr.rs +++ b/ghostscope-compiler/src/script/parser/expr.rs @@ -887,13 +887,25 @@ fn parse_postfix_access(pair: Pair) -> Result { .ok_or(ParseError::InvalidExpression)?; match suffix_inner.as_rule() { Rule::member_suffix => { - let field = suffix_inner + let member = suffix_inner .into_inner() .next() - .ok_or(ParseError::InvalidExpression)? - .as_str() - .to_string(); - expr = Expr::MemberAccess(Box::new(expr), field); + .ok_or(ParseError::InvalidExpression)?; + match member.as_rule() { + Rule::identifier => { + expr = Expr::MemberAccess(Box::new(expr), member.as_str().to_string()); + } + Rule::tuple_index => { + let index = member.as_str().parse::().map_err(|_| { + ParseError::TypeError(format!( + "tuple index is out of range: {}", + member.as_str() + )) + })?; + expr = Expr::TupleAccess(Box::new(expr), index); + } + _ => return Err(ParseError::UnexpectedToken(member.as_rule())), + } } Rule::index_suffix => { let index_pair = suffix_inner diff --git a/ghostscope-compiler/src/script/parser/tests.rs b/ghostscope-compiler/src/script/parser/tests.rs index 08993c00..13f8320e 100644 --- a/ghostscope-compiler/src/script/parser/tests.rs +++ b/ghostscope-compiler/src/script/parser/tests.rs @@ -53,6 +53,35 @@ trace foo { )); } +#[test] +fn parse_tuple_index_access_preserves_numeric_members() { + let script = r#" +trace foo { + print tuple_value.0; + print wrapper.1.value; +} +"#; + let program = parse(script).expect("parse should succeed"); + let Statement::TracePoint { body, .. } = &program.statements[0] else { + panic!("expected trace point"); + }; + assert!(matches!( + &body[0], + Statement::Print(PrintStatement::ComplexVariable(Expr::TupleAccess(base, 0))) + if matches!(base.as_ref(), Expr::Variable(name) if name == "tuple_value") + )); + assert!(matches!( + &body[1], + Statement::Print(PrintStatement::ComplexVariable(Expr::MemberAccess(base, field))) + if field == "value" + && matches!( + base.as_ref(), + Expr::TupleAccess(tuple, 1) + if matches!(tuple.as_ref(), Expr::Variable(name) if name == "wrapper") + ) + )); +} + #[test] fn parse_memcmp_with_dynamic_len() { let script = r#" diff --git a/ghostscope-dwarf/src/analyzer/plan_global.rs b/ghostscope-dwarf/src/analyzer/plan_global.rs index f55ebd4b..99e35de3 100644 --- a/ghostscope-dwarf/src/analyzer/plan_global.rs +++ b/ghostscope-dwarf/src/analyzer/plan_global.rs @@ -149,6 +149,9 @@ impl DwarfAnalyzer { provenance: Provenance, ) -> Result { let path_buf = module_path.as_ref().to_path_buf(); + let module = self.module_id_for_path(&path_buf).ok_or_else(|| { + anyhow::anyhow!("Module {} has no semantic module id", path_buf.display()) + })?; if let Some(module_data) = self.modules.get(&path_buf) { let items = vec![(cu_off, die_off)]; let vars = module_data.resolve_variables_by_offsets_at_address(0, &items)?; @@ -160,6 +163,7 @@ impl DwarfAnalyzer { path_buf.display() ) })?; + module_data.attach_variable_identity(module, cu_off, die_off, &mut var); if var.dwarf_type.is_none() { if let Some(ti) = module_data.shallow_type_for_variable_offsets(cu_off, die_off) { var.type_name = ti.type_name(); diff --git a/ghostscope-dwarf/src/analyzer/plan_pc.rs b/ghostscope-dwarf/src/analyzer/plan_pc.rs index 2c803a81..858dce93 100644 --- a/ghostscope-dwarf/src/analyzer/plan_pc.rs +++ b/ghostscope-dwarf/src/analyzer/plan_pc.rs @@ -196,11 +196,12 @@ impl DwarfAnalyzer { for segment in &path.segments { let pointer_type_name = plan.type_name.clone(); self.complete_unknown_pointer_target_type(module_path, &mut plan, &pointer_type_name); + let layout_segment = self.layout_access_segment(plan.type_id, segment)?; let projected_type_id = match plan.type_id { - Some(type_id) => self.projected_type_id(type_id, segment)?, + Some(type_id) => self.projected_type_id(type_id, &layout_segment)?, None => None, }; - plan = plan.plan_access_path(&VariableAccessPath::new(vec![segment.clone()]))?; + plan = plan.plan_resolved_access_segment(segment, &layout_segment)?; plan.type_id = projected_type_id; if matches!(segment, VariableAccessSegment::Dereference) { self.complete_unknown_pointer_target_type( diff --git a/ghostscope-dwarf/src/analyzer/type_context.rs b/ghostscope-dwarf/src/analyzer/type_context.rs index 9a48964e..cb1c4484 100644 --- a/ghostscope-dwarf/src/analyzer/type_context.rs +++ b/ghostscope-dwarf/src/analyzer/type_context.rs @@ -1,8 +1,10 @@ use super::DwarfAnalyzer; use crate::{ - CompilationUnitMetadata, CuId, ModuleId, PcContext, Result, SemanticType, TypeId, TypeOrigin, - VariableAccessSegment, VariableReadPlan, + member_layout, semantics::PlanError, strip_type_aliases, CompilationUnitMetadata, CuId, + MemberLayout, ModuleId, PcContext, Result, SemanticType, TypeId, TypeInfo, TypeLayoutError, + TypeOrigin, VariableAccessSegment, VariableReadPlan, }; +use std::path::Path; impl DwarfAnalyzer { /// Return language and producer metadata for a loaded compilation unit. @@ -68,6 +70,71 @@ impl DwarfAnalyzer { Ok(projected) } + /// Resolve a source-level tuple index using an exact DWARF type identity. + pub fn tuple_member_layout( + &self, + type_id: TypeId, + aggregate_type: &TypeInfo, + index: u32, + ) -> Result { + let layout_segment = + self.layout_access_segment(Some(type_id), &VariableAccessSegment::TupleIndex(index))?; + let VariableAccessSegment::Field(field) = layout_segment else { + return Err(anyhow::anyhow!( + "tuple projection did not resolve to a DWARF member" + )); + }; + + match member_layout(aggregate_type, &field) { + Ok(layout) => Ok(layout), + Err(TypeLayoutError::UnknownMember { type_name, .. }) => { + Err(PlanError::UnknownTupleIndex { index, type_name }.into()) + } + Err(error) => Err(error.into()), + } + } + + /// Resolve a source-level tuple index by module and aggregate name. + pub fn tuple_member_layout_in_module>( + &self, + module_path: P, + aggregate_type: &TypeInfo, + index: u32, + ) -> Result { + let module_path = self + .loaded_module_path_for(module_path) + .ok_or_else(|| anyhow::anyhow!("Module is not loaded for tuple projection"))?; + let module = self.module_id_for_path(module_path).ok_or_else(|| { + anyhow::anyhow!("Module {} has no semantic module id", module_path.display()) + })?; + let type_name = match strip_type_aliases(aggregate_type) { + TypeInfo::StructType { name, .. } => name, + other => { + return Err(PlanError::UnknownTupleIndex { + index, + type_name: other.type_name(), + } + .into()) + } + }; + let type_id = self + .modules + .get(module_path) + .and_then(|module_data| module_data.aggregate_type_id_by_name(module, type_name)) + .ok_or(PlanError::TupleIndexMissingTypeIdentity { index })?; + self.tuple_member_layout(type_id, aggregate_type, index) + } + + /// Project a stable type identity through one source-level access segment. + pub fn project_type_id( + &self, + current: TypeId, + segment: &VariableAccessSegment, + ) -> Result> { + let layout_segment = self.layout_access_segment(Some(current), segment)?; + self.projected_type_id(current, &layout_segment) + } + pub(super) fn projected_type_id( &self, current: TypeId, @@ -81,4 +148,19 @@ impl DwarfAnalyzer { .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? .projected_type_id(current, segment) } + + pub(super) fn layout_access_segment( + &self, + current: Option, + segment: &VariableAccessSegment, + ) -> Result { + let VariableAccessSegment::TupleIndex(index) = segment else { + return Ok(segment.clone()); + }; + let current = current.ok_or(PlanError::TupleIndexMissingTypeIdentity { index: *index })?; + let origin = self + .type_origin(current)? + .ok_or(PlanError::TupleIndexMissingTypeIdentity { index: *index })?; + crate::language::resolve_access_segment(&origin, segment) + } } diff --git a/ghostscope-dwarf/src/language/mod.rs b/ghostscope-dwarf/src/language/mod.rs new file mode 100644 index 00000000..57da78c1 --- /dev/null +++ b/ghostscope-dwarf/src/language/mod.rs @@ -0,0 +1,62 @@ +//! Source-language dispatch for semantic DWARF projections. + +mod rust; + +use crate::{semantics::PlanError, SourceLanguage, TypeOrigin, VariableAccessSegment}; + +pub(crate) fn resolve_access_segment( + origin: &TypeOrigin, + segment: &VariableAccessSegment, +) -> crate::Result { + match segment { + VariableAccessSegment::TupleIndex(index) => match origin.language { + SourceLanguage::Rust => Ok(rust::resolve_tuple_index(*index)), + language => Err(PlanError::TupleIndexUnsupportedLanguage { + index: *index, + language, + } + .into()), + }, + _ => Ok(segment.clone()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CuId, ModuleId}; + + fn origin(language: SourceLanguage) -> TypeOrigin { + TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + } + } + + #[test] + fn maps_rust_tuple_index_to_producer_field() { + assert_eq!( + resolve_access_segment( + &origin(SourceLanguage::Rust), + &VariableAccessSegment::TupleIndex(3), + ) + .unwrap(), + VariableAccessSegment::Field("__3".to_string()) + ); + } + + #[test] + fn rejects_tuple_index_for_non_rust_language() { + let error = resolve_access_segment( + &origin(SourceLanguage::C), + &VariableAccessSegment::TupleIndex(0), + ) + .unwrap_err(); + assert!(error + .to_string() + .contains("not supported for source language C")); + } +} diff --git a/ghostscope-dwarf/src/language/rust.rs b/ghostscope-dwarf/src/language/rust.rs new file mode 100644 index 00000000..40a6c928 --- /dev/null +++ b/ghostscope-dwarf/src/language/rust.rs @@ -0,0 +1,5 @@ +use crate::VariableAccessSegment; + +pub(super) fn resolve_tuple_index(index: u32) -> VariableAccessSegment { + VariableAccessSegment::Field(format!("__{index}")) +} diff --git a/ghostscope-dwarf/src/lib.rs b/ghostscope-dwarf/src/lib.rs index ed289c83..ad981f86 100644 --- a/ghostscope-dwarf/src/lib.rs +++ b/ghostscope-dwarf/src/lib.rs @@ -10,6 +10,7 @@ pub(crate) mod core; pub(crate) mod binary; pub(crate) mod dwarf_expr; pub(crate) mod index; +pub(crate) mod language; pub(crate) mod loader; pub(crate) mod objfile; pub(crate) mod parser; diff --git a/ghostscope-dwarf/src/objfile/type_context.rs b/ghostscope-dwarf/src/objfile/type_context.rs index cc0076ba..ed8b8c8e 100644 --- a/ghostscope-dwarf/src/objfile/type_context.rs +++ b/ghostscope-dwarf/src/objfile/type_context.rs @@ -224,6 +224,9 @@ fn projected_type_loc( VariableAccessSegment::Field(field) => { projected_member_type_loc(dwarf, type_name_index, loc, field) } + VariableAccessSegment::TupleIndex(index) => Err(anyhow::anyhow!( + "tuple index '.{index}' was not resolved to a DWARF member" + )), VariableAccessSegment::ArrayIndex(_) => { projected_element_type_loc(dwarf, type_name_index, loc, false) } @@ -255,6 +258,21 @@ impl LoadedObjfile { ) .map(|loc| loc.map(|loc| type_id_from_loc(current.module, loc))) } + + pub(crate) fn aggregate_type_id_by_name(&self, module: ModuleId, name: &str) -> Option { + [gimli::DW_TAG_structure_type, gimli::DW_TAG_class_type] + .into_iter() + .find_map(|tag| self.type_name_index.find_aggregate_definition(name, tag)) + .map(|loc| { + type_id_from_loc( + module, + TypeLoc { + cu_off: loc.cu_offset, + die_off: loc.die_offset, + }, + ) + }) + } } #[cfg(test)] diff --git a/ghostscope-dwarf/src/objfile/variables.rs b/ghostscope-dwarf/src/objfile/variables.rs index 2981f5df..625d345c 100644 --- a/ghostscope-dwarf/src/objfile/variables.rs +++ b/ghostscope-dwarf/src/objfile/variables.rs @@ -83,6 +83,38 @@ pub(super) fn complete_aggregate_declaration_entry( } impl LoadedObjfile { + pub(crate) fn attach_variable_identity( + &self, + module: crate::ModuleId, + cu_offset: gimli::DebugInfoOffset, + die_offset: gimli::UnitOffset, + variable: &mut crate::parser::ParsedVariable, + ) { + variable.declaration = Some(die_ref(module, cu_offset, die_offset)); + + let dwarf = self.dwarf(); + let Ok(header) = dwarf.unit_header(cu_offset) else { + return; + }; + let Ok(unit) = dwarf.unit(header) else { + return; + }; + let Ok(entry) = unit.entry(die_offset) else { + return; + }; + let Ok(Some(type_loc)) = resolve_type_ref_with_origins(dwarf, &entry, &unit) else { + return; + }; + + variable.type_id = Some(type_id(module, type_loc.cu_off, type_loc.die_off)); + if variable.dwarf_type.is_none() { + if let Some(ty) = self.detailed_shallow_type(type_loc.cu_off, type_loc.die_off) { + variable.type_name = ty.type_name(); + variable.dwarf_type = Some(ty); + } + } + } + fn find_innermost_inline_node(func: &FunctionBlocks, pc: u64) -> Option { let path = func.block_path_for_pc(pc); path.iter() @@ -413,7 +445,6 @@ impl LoadedObjfile { let var_refs = func.variables_at_pc_with_scope_depth(address); let cfi_index = self.unwind_info.cfi_index().cloned(); - let dwarf_ref = self.dwarf(); let mut variables = Vec::with_capacity(var_refs.len()); let mut diagnostics = Vec::new(); @@ -449,28 +480,12 @@ impl LoadedObjfile { let Some(mut variable) = resolved.pop() else { continue; }; - variable.declaration = Some(die_ref(module, var_ref.cu_offset, var_ref.die_offset)); - - if let Ok(header) = dwarf_ref.unit_header(var_ref.cu_offset) { - if let Ok(unit) = dwarf_ref.unit(header) { - if let Ok(entry) = unit.entry(var_ref.die_offset) { - if let Ok(Some(type_loc)) = - resolve_type_ref_with_origins(dwarf_ref, &entry, &unit) - { - variable.type_id = - Some(type_id(module, type_loc.cu_off, type_loc.die_off)); - if variable.dwarf_type.is_none() { - if let Some(ty) = - self.detailed_shallow_type(type_loc.cu_off, type_loc.die_off) - { - variable.type_name = ty.type_name(); - variable.dwarf_type = Some(ty); - } - } - } - } - } - } + self.attach_variable_identity( + module, + var_ref.cu_offset, + var_ref.die_offset, + &mut variable, + ); variables.push(variable); } diff --git a/ghostscope-dwarf/src/semantics/variable_plan/mod.rs b/ghostscope-dwarf/src/semantics/variable_plan/mod.rs index 1b1a2ea3..6aa1f94e 100644 --- a/ghostscope-dwarf/src/semantics/variable_plan/mod.rs +++ b/ghostscope-dwarf/src/semantics/variable_plan/mod.rs @@ -231,6 +231,10 @@ impl VariableAccessPath { suffix.push('.'); suffix.push_str(field); } + VariableAccessSegment::TupleIndex(index) => { + suffix.push('.'); + suffix.push_str(&index.to_string()); + } VariableAccessSegment::ArrayIndex(index) => { suffix.push('['); suffix.push_str(&index.to_string()); @@ -246,6 +250,7 @@ impl VariableAccessPath { #[derive(Debug, Clone, PartialEq, Eq)] pub enum VariableAccessSegment { Field(String), + TupleIndex(u32), ArrayIndex(i64), Dereference, } @@ -263,6 +268,21 @@ pub enum PlanError { members: String, }, + #[error("Tuple index '.{index}' requires DWARF type identity for language dispatch")] + TupleIndexMissingTypeIdentity { index: u32 }, + + #[error("Tuple index '.{index}' is not supported for source language {language:?}")] + TupleIndexUnsupportedLanguage { + index: u32, + language: crate::SourceLanguage, + }, + + #[error("Tuple index '.{index}' is not available on type '{type_name}'")] + UnknownTupleIndex { index: u32, type_name: String }, + + #[error("Tuple index '.{index}' must be resolved through DwarfAnalyzer")] + UnresolvedTupleIndex { index: u32 }, + #[error("array access requires array or pointer type, got '{type_name}'")] InvalidArrayAccess { type_name: String }, @@ -456,11 +476,36 @@ impl VariableReadPlan { pub fn plan_access_path(&self, path: &VariableAccessPath) -> Result { let mut plan = self.clone(); for segment in &path.segments { - plan = plan.plan_access_segment(segment)?; + plan = plan.plan_resolved_access_segment(segment, segment)?; } + Ok(plan) + } - plan.access_path.segments.extend(path.segments.clone()); - plan.name.push_str(&path.suffix()); + pub(crate) fn plan_resolved_access_segment( + &self, + semantic_segment: &VariableAccessSegment, + layout_segment: &VariableAccessSegment, + ) -> Result { + let mut plan = match self.plan_access_segment(layout_segment) { + Ok(plan) => plan, + Err(error) => { + if let VariableAccessSegment::TupleIndex(index) = semantic_segment { + if let Some(PlanError::UnknownMember { type_name, .. }) = + error.downcast_ref::() + { + return Err(PlanError::UnknownTupleIndex { + index: *index, + type_name: type_name.clone(), + } + .into()); + } + } + return Err(error); + } + }; + let semantic_path = VariableAccessPath::new(vec![semantic_segment.clone()]); + plan.access_path.segments.push(semantic_segment.clone()); + plan.name.push_str(&semantic_path.suffix()); Ok(plan) } @@ -494,6 +539,9 @@ impl VariableReadPlan { match segment { VariableAccessSegment::Field(field) => self.plan_field_access(&dwarf_type, field), + VariableAccessSegment::TupleIndex(index) => { + Err(PlanError::UnresolvedTupleIndex { index: *index }.into()) + } VariableAccessSegment::ArrayIndex(index) => self.plan_array_index(&dwarf_type, *index), VariableAccessSegment::Dereference => self.plan_pointer_deref(&dwarf_type), } diff --git a/ghostscope-dwarf/src/semantics/variable_plan/tests.rs b/ghostscope-dwarf/src/semantics/variable_plan/tests.rs index 9aa6a662..875887f9 100644 --- a/ghostscope-dwarf/src/semantics/variable_plan/tests.rs +++ b/ghostscope-dwarf/src/semantics/variable_plan/tests.rs @@ -547,6 +547,47 @@ fn field_access_adds_member_offset_and_type() { ); } +#[test] +fn resolved_tuple_access_uses_dwarf_field_but_preserves_source_path() { + let int_type = TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }; + let plan = typed_read_plan( + VariableLocation::Address(AddressExpr::constant(0x1000)), + TypeInfo::StructType { + name: "Pair".to_string(), + size: 8, + members: vec![StructMember { + name: "__1".to_string(), + member_type: int_type.clone(), + offset: 4, + bit_offset: None, + bit_size: None, + }], + }, + ); + + let planned = plan + .plan_resolved_access_segment( + &VariableAccessSegment::TupleIndex(1), + &VariableAccessSegment::Field("__1".to_string()), + ) + .expect("resolved tuple access"); + + assert_eq!(planned.name, "value.1"); + assert_eq!( + planned.access_path.segments, + vec![VariableAccessSegment::TupleIndex(1)] + ); + assert_eq!(planned.dwarf_type, Some(int_type)); + assert_eq!( + planned.location, + VariableLocation::Address(AddressExpr::constant(0x1004)) + ); +} + #[test] fn field_access_unknown_member_reports_known_members() { let int_type = TypeInfo::BaseType {