/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 | | } |