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
182                .entry(entry.stage.to_string())
183                .or_insert(0u64) += 1;
184            total_confidence += entry.confidence;
185        }
186
187        TraceSummary {
188            total_decisions: self.entries.len(),
189            avg_confidence: if self.entries.is_empty() {
190                0.0
191            } else {
192                total_confidence / self.entries.len() as f64
193            },
194            decisions_by_stage,
195        }
196    }
197}
198
199/// Summary statistics for a trace.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct TraceSummary {
202    /// Total number of decisions made
203    pub total_decisions: usize,
204    /// Average confidence across all decisions
205    pub avg_confidence: f64,
206    /// Number of decisions per pipeline stage
207    pub decisions_by_stage: std::collections::HashMap<String, u64>,
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_trace_collector_new_is_empty() {
216        let collector = TraceCollector::new();
217        assert!(collector.is_empty());
218        assert_eq!(collector.len(), 0);
219    }
220
221    #[test]
222    fn test_trace_collector_record_entry() {
223        let mut collector = TraceCollector::new();
224        collector.record(TraceEntry {
225            stage: PipelineStage::OwnershipInference,
226            source_location: Some("line 5".to_string()),
227            decision_type: DecisionType::PointerClassification,
228            chosen: "Box<i32>".to_string(),
229            alternatives: vec!["&i32".to_string()],
230            confidence: 0.9,
231            reason: "malloc detected".to_string(),
232        });
233
234        assert_eq!(collector.len(), 1);
235        assert!(!collector.is_empty());
236        assert_eq!(collector.entries()[0].chosen, "Box<i32>");
237    }
238
239    #[test]
240    fn test_trace_collector_to_json() {
241        let mut collector = TraceCollector::new();
242        collector.record(TraceEntry {
243            stage: PipelineStage::CodeGeneration,
244            source_location: None,
245            decision_type: DecisionType::TypeMapping,
246            chosen: "i32".to_string(),
247            alternatives: vec![],
248            confidence: 1.0,
249            reason: "direct mapping".to_string(),
250        });
251
252        let json = collector.to_json();
253        assert!(json.contains("i32"));
254        assert!(json.contains("code_generation"));
255    }
256
257    #[test]
258    fn test_trace_collector_filter_by_stage() {
259        let mut collector = TraceCollector::new();
260        collector.record(TraceEntry {
261            stage: PipelineStage::Parsing,
262            source_location: None,
263            decision_type: DecisionType::TypeMapping,
264            chosen: "int".to_string(),
265            alternatives: vec![],
266            confidence: 1.0,
267            reason: "parsed".to_string(),
268        });
269        collector.record(TraceEntry {
270            stage: PipelineStage::OwnershipInference,
271            source_location: None,
272            decision_type: DecisionType::PointerClassification,
273            chosen: "&i32".to_string(),
274            alternatives: vec![],
275            confidence: 0.8,
276            reason: "read-only".to_string(),
277        });
278
279        let parsing = collector.entries_for_stage(&PipelineStage::Parsing);
280        assert_eq!(parsing.len(), 1);
281
282        let ownership = collector.entries_for_stage(&PipelineStage::OwnershipInference);
283        assert_eq!(ownership.len(), 1);
284    }
285
286    #[test]
287    fn test_trace_summary() {
288        let mut collector = TraceCollector::new();
289        collector.record(TraceEntry {
290            stage: PipelineStage::OwnershipInference,
291            source_location: None,
292            decision_type: DecisionType::PointerClassification,
293            chosen: "Box<i32>".to_string(),
294            alternatives: vec![],
295            confidence: 0.8,
296            reason: "test".to_string(),
297        });
298        collector.record(TraceEntry {
299            stage: PipelineStage::OwnershipInference,
300            source_location: None,
301            decision_type: DecisionType::PointerClassification,
302            chosen: "&i32".to_string(),
303            alternatives: vec![],
304            confidence: 1.0,
305            reason: "test".to_string(),
306        });
307
308        let summary = collector.summary();
309        assert_eq!(summary.total_decisions, 2);
310        assert!((summary.avg_confidence - 0.9).abs() < 0.001);
311        assert_eq!(
312            summary.decisions_by_stage.get("ownership_inference"),
313            Some(&2)
314        );
315    }
316
317    // ============================================================================
318    // Additional coverage: Display impls
319    // ============================================================================
320
321    #[test]
322    fn test_pipeline_stage_display_all_variants() {
323        assert_eq!(format!("{}", PipelineStage::Parsing), "parsing");
324        assert_eq!(format!("{}", PipelineStage::HirConversion), "hir_conversion");
325        assert_eq!(
326            format!("{}", PipelineStage::OwnershipInference),
327            "ownership_inference"
328        );
329        assert_eq!(
330            format!("{}", PipelineStage::LifetimeAnalysis),
331            "lifetime_analysis"
332        );
333        assert_eq!(
334            format!("{}", PipelineStage::CodeGeneration),
335            "code_generation"
336        );
337    }
338
339    #[test]
340    fn test_decision_type_display_all_variants() {
341        assert_eq!(
342            format!("{}", DecisionType::PointerClassification),
343            "pointer_classification"
344        );
345        assert_eq!(format!("{}", DecisionType::TypeMapping), "type_mapping");
346        assert_eq!(
347            format!("{}", DecisionType::SafetyTransformation),
348            "safety_transformation"
349        );
350        assert_eq!(
351            format!("{}", DecisionType::LifetimeAnnotation),
352            "lifetime_annotation"
353        );
354        assert_eq!(
355            format!("{}", DecisionType::PatternDetection),
356            "pattern_detection"
357        );
358        assert_eq!(
359            format!("{}", DecisionType::SignatureTransformation),
360            "signature_transformation"
361        );
362    }
363
364    // ============================================================================
365    // Additional coverage: edge cases
366    // ============================================================================
367
368    #[test]
369    fn test_trace_summary_empty() {
370        let collector = TraceCollector::new();
371        let summary = collector.summary();
372        assert_eq!(summary.total_decisions, 0);
373        assert_eq!(summary.avg_confidence, 0.0);
374        assert!(summary.decisions_by_stage.is_empty());
375    }
376
377    #[test]
378    fn test_trace_collector_entries_for_stage_no_match() {
379        let mut collector = TraceCollector::new();
380        collector.record(TraceEntry {
381            stage: PipelineStage::Parsing,
382            source_location: None,
383            decision_type: DecisionType::TypeMapping,
384            chosen: "int".to_string(),
385            alternatives: vec![],
386            confidence: 1.0,
387            reason: "test".to_string(),
388        });
389
390        let codegen = collector.entries_for_stage(&PipelineStage::CodeGeneration);
391        assert!(codegen.is_empty());
392    }
393
394    #[test]
395    fn test_trace_collector_to_json_empty() {
396        let collector = TraceCollector::new();
397        let json = collector.to_json();
398        assert_eq!(json, "[]");
399    }
400
401    #[test]
402    fn test_trace_collector_multiple_stages() {
403        let mut collector = TraceCollector::new();
404        collector.record(TraceEntry {
405            stage: PipelineStage::Parsing,
406            source_location: Some("line 1".to_string()),
407            decision_type: DecisionType::TypeMapping,
408            chosen: "i32".to_string(),
409            alternatives: vec!["i64".to_string()],
410            confidence: 0.9,
411            reason: "int maps to i32".to_string(),
412        });
413        collector.record(TraceEntry {
414            stage: PipelineStage::HirConversion,
415            source_location: Some("line 5".to_string()),
416            decision_type: DecisionType::PatternDetection,
417            chosen: "for_loop".to_string(),
418            alternatives: vec!["while_loop".to_string()],
419            confidence: 0.85,
420            reason: "C for → Rust for".to_string(),
421        });
422        collector.record(TraceEntry {
423            stage: PipelineStage::LifetimeAnalysis,
424            source_location: None,
425            decision_type: DecisionType::LifetimeAnnotation,
426            chosen: "'a".to_string(),
427            alternatives: vec!["'static".to_string()],
428            confidence: 0.7,
429            reason: "scope analysis".to_string(),
430        });
431        collector.record(TraceEntry {
432            stage: PipelineStage::CodeGeneration,
433            source_location: Some("line 10".to_string()),
434            decision_type: DecisionType::SafetyTransformation,
435            chosen: "safe_indexing".to_string(),
436            alternatives: vec!["raw_pointer".to_string()],
437            confidence: 0.95,
438            reason: "bounds check possible".to_string(),
439        });
440        collector.record(TraceEntry {
441            stage: PipelineStage::OwnershipInference,
442            source_location: None,
443            decision_type: DecisionType::SignatureTransformation,
444            chosen: "&[i32]".to_string(),
445            alternatives: vec!["*const i32".to_string()],
446            confidence: 0.88,
447            reason: "array param to slice".to_string(),
448        });
449
450        assert_eq!(collector.len(), 5);
451
452        let summary = collector.summary();
453        assert_eq!(summary.total_decisions, 5);
454        assert_eq!(summary.decisions_by_stage.len(), 5);
455        assert_eq!(summary.decisions_by_stage.get("parsing"), Some(&1));
456        assert_eq!(summary.decisions_by_stage.get("hir_conversion"), Some(&1));
457        assert_eq!(summary.decisions_by_stage.get("lifetime_analysis"), Some(&1));
458        assert_eq!(summary.decisions_by_stage.get("code_generation"), Some(&1));
459        assert_eq!(
460            summary.decisions_by_stage.get("ownership_inference"),
461            Some(&1)
462        );
463
464        let json = collector.to_json();
465        assert!(json.contains("parsing"));
466        assert!(json.contains("hir_conversion"));
467        assert!(json.contains("lifetime_analysis"));
468        assert!(json.contains("safety_transformation"));
469        assert!(json.contains("signature_transformation"));
470    }
471
472    #[test]
473    fn test_trace_entry_serialization_roundtrip() {
474        let entry = TraceEntry {
475            stage: PipelineStage::OwnershipInference,
476            source_location: Some("test.c:42:5".to_string()),
477            decision_type: DecisionType::PointerClassification,
478            chosen: "Box<i32>".to_string(),
479            alternatives: vec!["&i32".to_string(), "&mut i32".to_string()],
480            confidence: 0.92,
481            reason: "single_alloc_single_free_pattern".to_string(),
482        };
483
484        let json = serde_json::to_string(&entry).unwrap();
485        let deserialized: TraceEntry = serde_json::from_str(&json).unwrap();
486        assert_eq!(deserialized.chosen, "Box<i32>");
487        assert_eq!(deserialized.alternatives.len(), 2);
488        assert_eq!(deserialized.confidence, 0.92);
489    }
490
491    #[test]
492    fn test_trace_summary_serialization() {
493        let mut collector = TraceCollector::new();
494        collector.record(TraceEntry {
495            stage: PipelineStage::Parsing,
496            source_location: None,
497            decision_type: DecisionType::TypeMapping,
498            chosen: "i32".to_string(),
499            alternatives: vec![],
500            confidence: 1.0,
501            reason: "test".to_string(),
502        });
503
504        let summary = collector.summary();
505        let json = serde_json::to_string(&summary).unwrap();
506        assert!(json.contains("total_decisions"));
507        assert!(json.contains("avg_confidence"));
508    }
509}