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
}