Skip to main content

decy_core/
trace.rs

1//! DECY-193: Decision tracing / flight recorder for transpilation.
2//!
3//! Provides a JSON audit trail of all decisions made during transpilation,
4//! including ownership inference, type mapping, and code generation choices.
5//!
6//! # Examples
7//!
8//! ```
9//! use decy_core::trace::{TraceCollector, TraceEntry, DecisionType, PipelineStage};
10//!
11//! let mut collector = TraceCollector::new();
12//! collector.record(TraceEntry {
13//!     stage: PipelineStage::OwnershipInference,
14//!     source_location: Some("line 10".to_string()),
15//!     decision_type: DecisionType::PointerClassification,
16//!     chosen: "Box<i32>".to_string(),
17//!     alternatives: vec!["&i32".to_string(), "&mut i32".to_string()],
18//!     confidence: 0.95,
19//!     reason: "malloc/free pattern detected".to_string(),
20//! });
21//!
22//! assert_eq!(collector.entries().len(), 1);
23//! ```
24
25use serde::{Deserialize, Serialize};
26
27/// Stage of the transpilation pipeline where a decision was made.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum PipelineStage {
31    /// C parsing stage
32    Parsing,
33    /// HIR conversion stage
34    HirConversion,
35    /// Ownership inference stage
36    OwnershipInference,
37    /// Lifetime analysis stage
38    LifetimeAnalysis,
39    /// Code generation stage
40    CodeGeneration,
41}
42
43impl std::fmt::Display for PipelineStage {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            PipelineStage::Parsing => write!(f, "parsing"),
47            PipelineStage::HirConversion => write!(f, "hir_conversion"),
48            PipelineStage::OwnershipInference => write!(f, "ownership_inference"),
49            PipelineStage::LifetimeAnalysis => write!(f, "lifetime_analysis"),
50            PipelineStage::CodeGeneration => write!(f, "code_generation"),
51        }
52    }
53}
54
55/// Type of decision being recorded.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum DecisionType {
59    /// Classification of a pointer as owning, borrowing, etc.
60    PointerClassification,
61    /// Mapping a C type to a Rust type
62    TypeMapping,
63    /// Choosing a safe pattern over an unsafe one
64    SafetyTransformation,
65    /// Lifetime annotation decision
66    LifetimeAnnotation,
67    /// Pattern detection (malloc/free, array, etc.)
68    PatternDetection,
69    /// Function signature transformation
70    SignatureTransformation,
71}
72
73impl std::fmt::Display for DecisionType {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            DecisionType::PointerClassification => write!(f, "pointer_classification"),
77            DecisionType::TypeMapping => write!(f, "type_mapping"),
78            DecisionType::SafetyTransformation => write!(f, "safety_transformation"),
79            DecisionType::LifetimeAnnotation => write!(f, "lifetime_annotation"),
80            DecisionType::PatternDetection => write!(f, "pattern_detection"),
81            DecisionType::SignatureTransformation => write!(f, "signature_transformation"),
82        }
83    }
84}
85
86/// A single decision recorded during transpilation.
87///
88/// Each entry captures what was decided, what alternatives existed,
89/// and why the chosen option was selected.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct TraceEntry {
92    /// Which pipeline stage made this decision
93    pub stage: PipelineStage,
94    /// Source location in the C code (e.g., "line 10, column 5")
95    pub source_location: Option<String>,
96    /// What type of decision was made
97    pub decision_type: DecisionType,
98    /// The option that was chosen
99    pub chosen: String,
100    /// Alternative options that were considered but not chosen
101    pub alternatives: Vec<String>,
102    /// Confidence level (0.0 to 1.0)
103    pub confidence: f64,
104    /// Human-readable reason for the decision
105    pub reason: String,
106}
107
108/// Collects trace entries during transpilation.
109///
110/// Thread-safe collector that can be passed through the pipeline stages.
111///
112/// # Examples
113///
114/// ```
115/// use decy_core::trace::{TraceCollector, TraceEntry, DecisionType, PipelineStage};
116///
117/// let mut collector = TraceCollector::new();
118/// assert!(collector.is_empty());
119///
120/// collector.record(TraceEntry {
121///     stage: PipelineStage::CodeGeneration,
122///     source_location: None,
123///     decision_type: DecisionType::TypeMapping,
124///     chosen: "i32".to_string(),
125///     alternatives: vec!["i64".to_string()],
126///     confidence: 1.0,
127///     reason: "C int maps to Rust i32".to_string(),
128/// });
129///
130/// assert_eq!(collector.len(), 1);
131/// let json = collector.to_json();
132/// assert!(json.contains("i32"));
133/// ```
134#[derive(Debug, Clone, Default)]
135pub struct TraceCollector {
136    entries: Vec<TraceEntry>,
137}
138
139impl TraceCollector {
140    /// Create a new empty trace collector.
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    /// Record a trace entry.
146    pub fn record(&mut self, entry: TraceEntry) {
147        self.entries.push(entry);
148    }
149
150    /// Get all recorded entries.
151    pub fn entries(&self) -> &[TraceEntry] {
152        &self.entries
153    }
154
155    /// Get the number of recorded entries.
156    pub fn len(&self) -> usize {
157        self.entries.len()
158    }
159
160    /// Check if the collector has no entries.
161    pub fn is_empty(&self) -> bool {
162        self.entries.is_empty()
163    }
164
165    /// Serialize all entries to JSON.
166    pub fn to_json(&self) -> String {
167        serde_json::to_string_pretty(&self.entries).unwrap_or_else(|_| "[]".to_string())
168    }
169
170    /// Filter entries by pipeline stage.
171    pub fn entries_for_stage(&self, stage: &PipelineStage) -> Vec<&TraceEntry> {
172        self.entries.iter().filter(|e| &e.stage == stage).collect()
173    }
174
175    /// Get summary statistics.
176    pub fn summary(&self) -> TraceSummary {
177        let mut decisions_by_stage = std::collections::HashMap::new();
178        let mut total_confidence = 0.0;
179
180        for entry in &self.entries {
181            *decisions_by_stage.entry(entry.stage.to_string()).or_insert(0u64) += 1;
182            total_confidence += entry.confidence;
183        }
184
185        TraceSummary {
186            total_decisions: self.entries.len(),
187            avg_confidence: if self.entries.is_empty() {
188                0.0
189            } else {
190                total_confidence / self.entries.len() as f64
191            },
192            decisions_by_stage,
193        }
194    }
195}
196
197/// Summary statistics for a trace.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TraceSummary {
200    /// Total number of decisions made
201    pub total_decisions: usize,
202    /// Average confidence across all decisions
203    pub avg_confidence: f64,
204    /// Number of decisions per pipeline stage
205    pub decisions_by_stage: std::collections::HashMap<String, u64>,
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_trace_collector_new_is_empty() {
214        let collector = TraceCollector::new();
215        assert!(collector.is_empty());
216        assert_eq!(collector.len(), 0);
217    }
218
219    #[test]
220    fn test_trace_collector_record_entry() {
221        let mut collector = TraceCollector::new();
222        collector.record(TraceEntry {
223            stage: PipelineStage::OwnershipInference,
224            source_location: Some("line 5".to_string()),
225            decision_type: DecisionType::PointerClassification,
226            chosen: "Box<i32>".to_string(),
227            alternatives: vec!["&i32".to_string()],
228            confidence: 0.9,
229            reason: "malloc detected".to_string(),
230        });
231
232        assert_eq!(collector.len(), 1);
233        assert!(!collector.is_empty());
234        assert_eq!(collector.entries()[0].chosen, "Box<i32>");
235    }
236
237    #[test]
238    fn test_trace_collector_to_json() {
239        let mut collector = TraceCollector::new();
240        collector.record(TraceEntry {
241            stage: PipelineStage::CodeGeneration,
242            source_location: None,
243            decision_type: DecisionType::TypeMapping,
244            chosen: "i32".to_string(),
245            alternatives: vec![],
246            confidence: 1.0,
247            reason: "direct mapping".to_string(),
248        });
249
250        let json = collector.to_json();
251        assert!(json.contains("i32"));
252        assert!(json.contains("code_generation"));
253    }
254
255    #[test]
256    fn test_trace_collector_filter_by_stage() {
257        let mut collector = TraceCollector::new();
258        collector.record(TraceEntry {
259            stage: PipelineStage::Parsing,
260            source_location: None,
261            decision_type: DecisionType::TypeMapping,
262            chosen: "int".to_string(),
263            alternatives: vec![],
264            confidence: 1.0,
265            reason: "parsed".to_string(),
266        });
267        collector.record(TraceEntry {
268            stage: PipelineStage::OwnershipInference,
269            source_location: None,
270            decision_type: DecisionType::PointerClassification,
271            chosen: "&i32".to_string(),
272            alternatives: vec![],
273            confidence: 0.8,
274            reason: "read-only".to_string(),
275        });
276
277        let parsing = collector.entries_for_stage(&PipelineStage::Parsing);
278        assert_eq!(parsing.len(), 1);
279
280        let ownership = collector.entries_for_stage(&PipelineStage::OwnershipInference);
281        assert_eq!(ownership.len(), 1);
282    }
283
284    #[test]
285    fn test_trace_summary() {
286        let mut collector = TraceCollector::new();
287        collector.record(TraceEntry {
288            stage: PipelineStage::OwnershipInference,
289            source_location: None,
290            decision_type: DecisionType::PointerClassification,
291            chosen: "Box<i32>".to_string(),
292            alternatives: vec![],
293            confidence: 0.8,
294            reason: "test".to_string(),
295        });
296        collector.record(TraceEntry {
297            stage: PipelineStage::OwnershipInference,
298            source_location: None,
299            decision_type: DecisionType::PointerClassification,
300            chosen: "&i32".to_string(),
301            alternatives: vec![],
302            confidence: 1.0,
303            reason: "test".to_string(),
304        });
305
306        let summary = collector.summary();
307        assert_eq!(summary.total_decisions, 2);
308        assert!((summary.avg_confidence - 0.9).abs() < 0.001);
309        assert_eq!(summary.decisions_by_stage.get("ownership_inference"), Some(&2));
310    }
311
312    // ============================================================================
313    // Additional coverage: Display impls
314    // ============================================================================
315
316    #[test]
317    fn test_pipeline_stage_display_all_variants() {
318        assert_eq!(format!("{}", PipelineStage::Parsing), "parsing");
319        assert_eq!(format!("{}", PipelineStage::HirConversion), "hir_conversion");
320        assert_eq!(format!("{}", PipelineStage::OwnershipInference), "ownership_inference");
321        assert_eq!(format!("{}", PipelineStage::LifetimeAnalysis), "lifetime_analysis");
322        assert_eq!(format!("{}", PipelineStage::CodeGeneration), "code_generation");
323    }
324
325    #[test]
326    fn test_decision_type_display_all_variants() {
327        assert_eq!(format!("{}", DecisionType::PointerClassification), "pointer_classification");
328        assert_eq!(format!("{}", DecisionType::TypeMapping), "type_mapping");
329        assert_eq!(format!("{}", DecisionType::SafetyTransformation), "safety_transformation");
330        assert_eq!(format!("{}", DecisionType::LifetimeAnnotation), "lifetime_annotation");
331        assert_eq!(format!("{}", DecisionType::PatternDetection), "pattern_detection");
332        assert_eq!(
333            format!("{}", DecisionType::SignatureTransformation),
334            "signature_transformation"
335        );
336    }
337
338    // ============================================================================
339    // Additional coverage: edge cases
340    // ============================================================================
341
342    #[test]
343    fn test_trace_summary_empty() {
344        let collector = TraceCollector::new();
345        let summary = collector.summary();
346        assert_eq!(summary.total_decisions, 0);
347        assert_eq!(summary.avg_confidence, 0.0);
348        assert!(summary.decisions_by_stage.is_empty());
349    }
350
351    #[test]
352    fn test_trace_collector_entries_for_stage_no_match() {
353        let mut collector = TraceCollector::new();
354        collector.record(TraceEntry {
355            stage: PipelineStage::Parsing,
356            source_location: None,
357            decision_type: DecisionType::TypeMapping,
358            chosen: "int".to_string(),
359            alternatives: vec![],
360            confidence: 1.0,
361            reason: "test".to_string(),
362        });
363
364        let codegen = collector.entries_for_stage(&PipelineStage::CodeGeneration);
365        assert!(codegen.is_empty());
366    }
367
368    #[test]
369    fn test_trace_collector_to_json_empty() {
370        let collector = TraceCollector::new();
371        let json = collector.to_json();
372        assert_eq!(json, "[]");
373    }
374
375    #[test]
376    fn test_trace_collector_multiple_stages() {
377        let mut collector = TraceCollector::new();
378        collector.record(TraceEntry {
379            stage: PipelineStage::Parsing,
380            source_location: Some("line 1".to_string()),
381            decision_type: DecisionType::TypeMapping,
382            chosen: "i32".to_string(),
383            alternatives: vec!["i64".to_string()],
384            confidence: 0.9,
385            reason: "int maps to i32".to_string(),
386        });
387        collector.record(TraceEntry {
388            stage: PipelineStage::HirConversion,
389            source_location: Some("line 5".to_string()),
390            decision_type: DecisionType::PatternDetection,
391            chosen: "for_loop".to_string(),
392            alternatives: vec!["while_loop".to_string()],
393            confidence: 0.85,
394            reason: "C for → Rust for".to_string(),
395        });
396        collector.record(TraceEntry {
397            stage: PipelineStage::LifetimeAnalysis,
398            source_location: None,
399            decision_type: DecisionType::LifetimeAnnotation,
400            chosen: "'a".to_string(),
401            alternatives: vec!["'static".to_string()],
402            confidence: 0.7,
403            reason: "scope analysis".to_string(),
404        });
405        collector.record(TraceEntry {
406            stage: PipelineStage::CodeGeneration,
407            source_location: Some("line 10".to_string()),
408            decision_type: DecisionType::SafetyTransformation,
409            chosen: "safe_indexing".to_string(),
410            alternatives: vec!["raw_pointer".to_string()],
411            confidence: 0.95,
412            reason: "bounds check possible".to_string(),
413        });
414        collector.record(TraceEntry {
415            stage: PipelineStage::OwnershipInference,
416            source_location: None,
417            decision_type: DecisionType::SignatureTransformation,
418            chosen: "&[i32]".to_string(),
419            alternatives: vec!["*const i32".to_string()],
420            confidence: 0.88,
421            reason: "array param to slice".to_string(),
422        });
423
424        assert_eq!(collector.len(), 5);
425
426        let summary = collector.summary();
427        assert_eq!(summary.total_decisions, 5);
428        assert_eq!(summary.decisions_by_stage.len(), 5);
429        assert_eq!(summary.decisions_by_stage.get("parsing"), Some(&1));
430        assert_eq!(summary.decisions_by_stage.get("hir_conversion"), Some(&1));
431        assert_eq!(summary.decisions_by_stage.get("lifetime_analysis"), Some(&1));
432        assert_eq!(summary.decisions_by_stage.get("code_generation"), Some(&1));
433        assert_eq!(summary.decisions_by_stage.get("ownership_inference"), Some(&1));
434
435        let json = collector.to_json();
436        assert!(json.contains("parsing"));
437        assert!(json.contains("hir_conversion"));
438        assert!(json.contains("lifetime_analysis"));
439        assert!(json.contains("safety_transformation"));
440        assert!(json.contains("signature_transformation"));
441    }
442
443    #[test]
444    fn test_trace_entry_serialization_roundtrip() {
445        let entry = TraceEntry {
446            stage: PipelineStage::OwnershipInference,
447            source_location: Some("test.c:42:5".to_string()),
448            decision_type: DecisionType::PointerClassification,
449            chosen: "Box<i32>".to_string(),
450            alternatives: vec!["&i32".to_string(), "&mut i32".to_string()],
451            confidence: 0.92,
452            reason: "single_alloc_single_free_pattern".to_string(),
453        };
454
455        let json = serde_json::to_string(&entry).unwrap();
456        let deserialized: TraceEntry = serde_json::from_str(&json).unwrap();
457        assert_eq!(deserialized.chosen, "Box<i32>");
458        assert_eq!(deserialized.alternatives.len(), 2);
459        assert_eq!(deserialized.confidence, 0.92);
460    }
461
462    #[test]
463    fn test_trace_summary_serialization() {
464        let mut collector = TraceCollector::new();
465        collector.record(TraceEntry {
466            stage: PipelineStage::Parsing,
467            source_location: None,
468            decision_type: DecisionType::TypeMapping,
469            chosen: "i32".to_string(),
470            alternatives: vec![],
471            confidence: 1.0,
472            reason: "test".to_string(),
473        });
474
475        let summary = collector.summary();
476        let json = serde_json::to_string(&summary).unwrap();
477        assert!(json.contains("total_decisions"));
478        assert!(json.contains("avg_confidence"));
479    }
480}