## Coverage Report

#### Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/ruchy/src/transpiler/provenance.rs

    Line
    Count
    Source
    1
    //! Compilation Provenance Tracking
    2
    //!
    3
    //! Based on docs/ruchy-transpiler-docs.md Section 7: Compilation Provenance Tracking
    4
    //! Complete audit trail of compilation decisions
    5
    6
    use serde::{Deserialize, Serialize};
    7
    use sha2::{Digest, Sha256};
    8
    use std::time::Instant;
    9
    10
    /// Complete trace of a compilation
    11
    #[derive(Debug, Serialize, Deserialize)]
    12
    pub struct CompilationTrace {
    13
        /// SHA256 hash of the source code
    14
        pub source_hash: String,
    15
        /// All transformations applied
    16
        pub transformations: Vec<Transformation>,
    17
        /// Total compilation time
    18
        pub total_duration_ns: u64,
    19
        /// Metadata about the compilation
    20
        pub metadata: CompilationMetadata,
    21
    }
    22
    23
    /// Metadata about the compilation environment
    24
    #[derive(Debug, Clone, Serialize, Deserialize)]
    25
    pub struct CompilationMetadata {
    26
        pub ruchy_version: String,
    27
        pub rustc_version: String,
    28
        pub timestamp: String,
    29
        pub deterministic_seed: u64,
    30
        pub optimization_level: String,
    31
    }
    32
    33
    /// A single transformation pass
    34
    #[derive(Debug, Serialize, Deserialize)]
    35
    pub struct Transformation {
    36
        /// Name of the transformation pass
    37
        pub pass: String,
    38
        /// Hash of input to this pass
    39
        pub input_hash: String,
    40
        /// Hash of output from this pass
    41
        pub output_hash: String,
    42
        /// Rules applied during this transformation
    43
        pub rules_applied: Vec<Rule>,
    44
        /// Time taken for this pass
    45
        pub duration_ns: u64,
    46
    }
    47
    48
    /// A single rule application
    49
    #[derive(Debug, Serialize, Deserialize)]
    50
    pub struct Rule {
    51
        /// Name of the rule
    52
        pub name: String,
    53
        /// Source location where rule was applied
    54
        pub location: SourceSpan,
    55
        /// Code before transformation
    56
        pub before: String,
    57
        /// Code after transformation
    58
        pub after: String,
    59
    }
    60
    61
    #[derive(Debug, Clone, Serialize, Deserialize)]
    62
    pub struct SourceSpan {
    63
        pub file: Option<String>,
    64
        pub line_start: usize,
    65
        pub line_end: usize,
    66
        pub column_start: usize,
    67
        pub column_end: usize,
    68
    }
    69
    70
    /// Provenance tracker that records all compilation decisions
    71
    pub struct ProvenanceTracker {
    72
        source_hash: String,
    73
        transformations: Vec<Transformation>,
    74
        current_transformation: Option<TransformationBuilder>,
    75
        start_time: Instant,
    76
    }
    77
    78
    impl ProvenanceTracker {
    79
        /// Create a new provenance tracker for the given source
    80
        #[must_use]
    81
    0
        pub fn new(source: &str) -> Self {
    82
    0
            Self {
    83
    0
                source_hash: Self::hash(source),
    84
    0
                transformations: Vec::new(),
    85
    0
                current_transformation: None,
    86
    0
                start_time: Instant::now(),
    87
    0
            }
    88
    0
        }
    89
    90
        /// Start tracking a new transformation pass
    91
    0
        pub fn begin_pass(&mut self, name: &str, input: &str) {
    92
    0
            if let Some(builder) = self.current_transformation.take() {
    93
    0
                self.transformations.push(builder.finish());
    94
    0
            }
    95
    96
    0
            self.current_transformation = Some(TransformationBuilder::new(name, input));
    97
    0
        }
    98
    99
        /// Record a rule application
    100
    0
        pub fn record_rule(&mut self, rule: Rule) {
    101
    0
            if let Some(ref mut builder) = self.current_transformation {
    102
    0
                builder.add_rule(rule);
    103
    0
            }
    104
    0
        }
    105
    106
        /// Finish the current pass
    107
    0
        pub fn end_pass(&mut self, output: &str) {
    108
    0
            if let Some(mut builder) = self.current_transformation.take() {
    109
    0
                builder.set_output(output);
    110
    0
                self.transformations.push(builder.finish());
    111
    0
            }
    112
    0
        }
    113
    114
        /// Generate the complete compilation trace
    115
        #[must_use]
    116
        #[allow(clippy::cast_possible_truncation)]
    117
    0
        pub fn finish(mut self) -> CompilationTrace {
    118
            // Finish any pending transformation
    119
    0
            if let Some(builder) = self.current_transformation.take() {
    120
    0
                self.transformations.push(builder.finish());
    121
    0
            }
    122
    123
    0
            CompilationTrace {
    124
    0
                source_hash: self.source_hash,
    125
    0
                transformations: self.transformations,
    126
    0
                total_duration_ns: self
    127
    0
                    .start_time
    128
    0
                    .elapsed()
    129
    0
                    .as_nanos()
    130
    0
                    .min(u128::from(u64::MAX)) as u64,
    131
    0
                metadata: CompilationMetadata {
    132
    0
                    ruchy_version: env!("CARGO_PKG_VERSION").to_string(),
    133
    0
                    rustc_version: "1.75.0".to_string(), // Would get from rustc --version
    134
    0
                    timestamp: chrono::Utc::now().to_rfc3339(),
    135
    0
                    deterministic_seed: 42, // Would be configurable
    136
    0
                    optimization_level: "O2".to_string(),
    137
    0
                },
    138
    0
            }
    139
    0
        }
    140
    141
        /// Calculate SHA256 hash
    142
    0
        fn hash(s: &str) -> String {
    143
    0
            let mut hasher = Sha256::new();
    144
    0
            hasher.update(s.as_bytes());
    145
    0
            format!("{:x}", hasher.finalize())
    146
    0
        }
    147
    }
    148
    149
    /// Builder for a transformation
    150
    struct TransformationBuilder {
    151
        pass: String,
    152
        input_hash: String,
    153
        output_hash: Option<String>,
    154
        rules_applied: Vec<Rule>,
    155
        start_time: Instant,
    156
    }
    157
    158
    #[allow(clippy::cast_possible_truncation)]
    159
    impl TransformationBuilder {
    160
    0
        fn new(pass: &str, input: &str) -> Self {
    161
    0
            Self {
    162
    0
                pass: pass.to_string(),
    163
    0
                input_hash: ProvenanceTracker::hash(input),
    164
    0
                output_hash: None,
    165
    0
                rules_applied: Vec::new(),
    166
    0
                start_time: Instant::now(),
    167
    0
            }
    168
    0
        }
    169
    170
    0
        fn add_rule(&mut self, rule: Rule) {
    171
    0
            self.rules_applied.push(rule);
    172
    0
        }
    173
    174
    0
        fn set_output(&mut self, output: &str) {
    175
    0
            self.output_hash = Some(ProvenanceTracker::hash(output));
    176
    0
        }
    177
    178
    0
        fn finish(self) -> Transformation {
    179
            Transformation {
    180
    0
                pass: self.pass,
    181
    0
                input_hash: self.input_hash,
    182
    0
                output_hash: self.output_hash.unwrap_or_else(|| "incomplete".to_string()),
    183
    0
                rules_applied: self.rules_applied,
    184
    0
                duration_ns: self
    185
    0
                    .start_time
    186
    0
                    .elapsed()
    187
    0
                    .as_nanos()
    188
    0
                    .min(u128::from(u64::MAX)) as u64,
    189
            }
    190
    0
        }
    191
    }
    192
    193
    /// Compare two compilation traces to find divergence
    194
    pub struct TraceDiffer {
    195
        trace1: CompilationTrace,
    196
        trace2: CompilationTrace,
    197
    }
    198
    199
    impl TraceDiffer {
    200
        #[must_use]
    201
    0
        pub fn new(trace1: CompilationTrace, trace2: CompilationTrace) -> Self {
    202
    0
            Self { trace1, trace2 }
    203
    0
        }
    204
    205
        /// Find the first point where the traces diverge
    206
        #[must_use]
    207
    0
        pub fn find_divergence(&self) -> Option<DivergencePoint> {
    208
            // Check source hash
    209
    0
            if self.trace1.source_hash != self.trace2.source_hash {
    210
    0
                return Some(DivergencePoint {
    211
    0
                    stage: "source".to_string(),
    212
    0
                    pass_index: 0,
    213
    0
                    description: format!(
    214
    0
                        "Different source files: {} vs {}",
    215
    0
                        self.trace1.source_hash, self.trace2.source_hash
    216
    0
                    ),
    217
    0
                });
    218
    0
            }
    219
    220
            // Check each transformation
    221
    0
            for (i, (t1, t2)) in self
    222
    0
                .trace1
    223
    0
                .transformations
    224
    0
                .iter()
    225
    0
                .zip(self.trace2.transformations.iter())
    226
    0
                .enumerate()
    227
            {
    228
    0
                if t1.pass != t2.pass {
    229
    0
                    return Some(DivergencePoint {
    230
    0
                        stage: "transformation".to_string(),
    231
    0
                        pass_index: i,
    232
    0
                        description: format!(
    233
    0
                            "Different pass at index {}: {} vs {}",
    234
    0
                            i, t1.pass, t2.pass
    235
    0
                        ),
    236
    0
                    });
    237
    0
                }
    238
    239
    0
                if t1.output_hash != t2.output_hash {
    240
    0
                    return Some(DivergencePoint {
    241
    0
                        stage: "transformation".to_string(),
    242
    0
                        pass_index: i,
    243
    0
                        description: format!(
    244
    0
                            "Different output in pass '{}': {} vs {}",
    245
    0
                            t1.pass, t1.output_hash, t2.output_hash
    246
    0
                        ),
    247
    0
                    });
    248
    0
                }
    249
            }
    250
    251
    0
            None
    252
    0
        }
    253
    }
    254
    255
    #[derive(Debug)]
    256
    pub struct DivergencePoint {
    257
        pub stage: String,
    258
        pub pass_index: usize,
    259
        pub description: String,
    260
    }
    261
    262
    /// Integration with the transpiler
    263
    impl crate::Transpiler {
    264
        /// Transpile with provenance tracking
    265
    0
        pub fn transpile_with_provenance(
    266
    0
            &self,
    267
    0
            expr: &crate::Expr,
    268
    0
        ) -> (
    269
    0
            Result<proc_macro2::TokenStream, anyhow::Error>,
    270
    0
            CompilationTrace,
    271
    0
        ) {
    272
    0
            let source = format!("{expr:?}"); // Simplified - would serialize properly
    273
    0
            let mut tracker = ProvenanceTracker::new(&source);
    274
    275
            // Track the transpilation
    276
    0
            tracker.begin_pass("transpile", &source);
    277
    278
    0
            let result = self.transpile(expr);
    279
    280
    0
            if let Ok(ref tokens) = result {
    281
    0
                tracker.end_pass(&format!("{tokens}"));
    282
    0
            } else {
    283
    0
                tracker.end_pass("error");
    284
    0
            }
    285
    286
    0
            (result, tracker.finish())
    287
    0
        }
    288
    }
    289
    290
    #[cfg(test)]
    291
    #[allow(clippy::unwrap_used, clippy::panic)]
    292
    mod tests {
    293
        use super::*;
    294
    295
        #[test]
    296
        fn test_provenance_tracking() {
    297
            let mut tracker = ProvenanceTracker::new("let x = 10");
    298
    299
            tracker.begin_pass("parse", "let x = 10");
    300
            tracker.record_rule(Rule {
    301
                name: "let_statement".to_string(),
    302
                location: SourceSpan {
    303
                    file: None,
    304
                    line_start: 1,
    305
                    line_end: 1,
    306
                    column_start: 0,
    307
                    column_end: 10,
    308
                },
    309
                before: "let x = 10".to_string(),
    310
                after: "Let { name: \"x\", value: 10 }".to_string(),
    311
            });
    312
            tracker.end_pass("Let { name: \"x\", value: 10 }");
    313
    314
            tracker.begin_pass("normalize", "Let { name: \"x\", value: 10 }");
    315
            tracker.end_pass("Let { name: \"x\", value: Literal(10), body: Unit }");
    316
    317
            let trace = tracker.finish();
    318
    319
            assert_eq!(trace.transformations.len(), 2);
    320
            assert_eq!(trace.transformations[0].pass, "parse");
    321
            assert_eq!(trace.transformations[1].pass, "normalize");
    322
        }
    323
    324
        #[test]
    325
        fn test_trace_differ() {
    326
            let trace1 = CompilationTrace {
    327
                source_hash: "abc".to_string(),
    328
                transformations: vec![Transformation {
    329
                    pass: "parse".to_string(),
    330
                    input_hash: "in1".to_string(),
    331
                    output_hash: "out1".to_string(),
    332
                    rules_applied: vec![],
    333
                    duration_ns: 1000,
    334
                }],
    335
                total_duration_ns: 2000,
    336
                metadata: CompilationMetadata {
    337
                    ruchy_version: "1.0.0".to_string(),
    338
                    rustc_version: "1.75.0".to_string(),
    339
                    timestamp: "2024-01-01".to_string(),
    340
                    deterministic_seed: 42,
    341
                    optimization_level: "O2".to_string(),
    342
                },
    343
            };
    344
    345
            let trace2 = CompilationTrace {
    346
                source_hash: "abc".to_string(),
    347
                transformations: vec![Transformation {
    348
                    pass: "parse".to_string(),
    349
                    input_hash: "in1".to_string(),
    350
                    output_hash: "out2".to_string(), // Different output
    351
                    rules_applied: vec![],
    352
                    duration_ns: 1000,
    353
                }],
    354
                total_duration_ns: 2000,
    355
                metadata: trace1.metadata.clone(),
    356
            };
    357
    358
            let differ = TraceDiffer::new(trace1, trace2);
    359
            let divergence = differ.find_divergence();
    360
    361
            assert!(divergence.is_some());
    362
            let point = divergence.unwrap();
    363
            assert_eq!(point.stage, "transformation");
    364
            assert_eq!(point.pass_index, 0);
    365
        }
    366
    }