1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
#![cfg_attr(coverage_nightly, coverage(off))]
// TRACE-005: Execution Recording Infrastructure
// Sprint 72 - GREEN Phase: In-memory snapshot capture
// Sprint 76 - GREEN Phase: CAPTURE-001 RecordingWriter Integration
//
// Implements execution recording that captures program state at each step.
// Sprint 72 provided in-memory snapshot storage for time-travel debugging.
// Sprint 76 adds optional persistence to .pmat files via RecordingWriter.
//
// Integration Modes:
// 1. Memory-Only: ExecutionRecorder::new() - Sprint 72 backward compatible
// 2. Streaming to File: ExecutionRecorder::with_writer() - Sprint 76 persistence
//
// The recorder is generic over Write trait, enabling flexible output:
// - File::create("session.pmat") for file persistence
// - Cursor::new(Vec::new()) for in-memory .pmat generation
// - TcpStream for network streaming (future)
use super::recording::{RecordingWriter, Snapshot};
use super::server::DapServer;
use super::types::{ExecutionSnapshot, SourceLocation, StackFrame};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::io::Write;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
/// Execution Recorder manages recording of program execution state
///
/// Sprint 76: Now supports optional persistence via RecordingWriter<W>
pub struct ExecutionRecorder<W: Write = std::io::Sink> {
/// All snapshots in chronological order (in-memory)
snapshots: Vec<ExecutionSnapshot>,
/// Current recording state
is_recording: bool,
/// Integration with DAP server
dap_server: Arc<Mutex<DapServer>>,
/// Optional recording writer for persistence (Sprint 76)
writer: Option<RecordingWriter<W>>,
}
impl<W: Write> ExecutionRecorder<W> {
/// Create a new execution recorder with RecordingWriter for persistence
///
/// Sprint 76 - CAPTURE-001: This enables automatic snapshot writing to .pmat files
///
/// # Arguments
/// * `writer` - Any type implementing Write trait (File, Cursor, TcpStream, etc.)
/// * `program` - Program name for recording metadata
/// * `args` - Command-line arguments for recording metadata
/// * `dap_server` - DAP server for capturing execution state
///
/// # Example
/// ```rust,no_run
/// use std::fs::File;
/// use std::sync::{Arc, Mutex};
/// use pmat::services::dap::{ExecutionRecorder, DapServer};
///
/// let file = File::create("session.pmat").expect("internal error");
/// let dap = Arc::new(Mutex::new(DapServer::new()));
/// let mut recorder = ExecutionRecorder::with_writer(
/// file,
/// "my_program".to_string(),
/// vec!["arg1".to_string(), "arg2".to_string()],
/// dap,
/// ).expect("internal error");
///
/// recorder.start_recording();
/// // ... capture snapshots during execution ...
/// recorder.finalize().expect("internal error");
/// ```
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_writer(
writer: W,
program: String,
args: Vec<String>,
dap_server: Arc<Mutex<DapServer>>,
) -> Result<Self> {
let recording_writer = RecordingWriter::new(writer, program, args)
.context("Failed to create RecordingWriter")?;
Ok(Self {
snapshots: Vec::new(),
is_recording: false,
dap_server,
writer: Some(recording_writer),
})
}
/// Add environment variable to recording metadata
///
/// Sprint 76 - CAPTURE-001: Enriches recording metadata
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn add_environment(&mut self, key: impl Into<String>, value: impl Into<String>) {
if let Some(ref mut writer) = self.writer {
writer.add_environment(key, value);
}
}
/// Finalize the recording (must be called to complete .pmat file)
///
/// Sprint 76 - CAPTURE-001: Completes the recording and flushes to disk
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn finalize(self) -> Result<()> {
if let Some(writer) = self.writer {
writer.finalize().context("Failed to finalize recording")?;
}
Ok(())
}
/// Convert ExecutionSnapshot (Sprint 72) to Snapshot (Sprint 75)
///
/// Maps between in-memory snapshot format and .pmat file format
fn convert_to_recording_snapshot(exec_snapshot: &ExecutionSnapshot) -> Snapshot {
let stack_frames = exec_snapshot
.call_stack
.iter()
.map(|frame| {
let file = frame.source.as_ref().and_then(|s| s.path.clone());
let line = if frame.line >= 0 {
Some(frame.line as u32)
} else {
None
};
super::recording::StackFrame {
name: frame.name.clone(),
file,
line,
locals: HashMap::new(),
}
})
.collect();
let timestamp_relative_ms = (exec_snapshot.timestamp / 1_000_000) as u32;
let frame_id = exec_snapshot.sequence as u64;
let instruction_pointer = 0u64;
Snapshot {
frame_id,
timestamp_relative_ms,
variables: exec_snapshot.variables.clone(),
stack_frames,
instruction_pointer,
memory_snapshot: None,
}
}
/// Capture a snapshot of current execution state
///
/// Sprint 76: Now also writes to RecordingWriter if present
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn capture_snapshot(&mut self) -> Result<ExecutionSnapshot, String> {
if !self.is_recording {
return Err("Not recording".to_string());
}
let dap = self
.dap_server
.lock()
.map_err(|e| format!("Failed to lock DAP server: {}", e))?;
let stopped_file = dap
.current_stopped_file()
.ok_or_else(|| "No file currently stopped at".to_string())?;
let stopped_line = dap
.current_stopped_line()
.ok_or_else(|| "No line currently stopped at".to_string())?;
let variables_vec = dap
.get_variables_at_line(&stopped_file, stopped_line)
.map_err(|e| format!("Failed to get variables: {}", e))?;
let mut variables = HashMap::new();
for var in variables_vec {
variables.insert(
var.name.clone(),
serde_json::json!({
"value": var.value,
"type": var.type_info
}),
);
}
let call_stack = vec![StackFrame {
id: 1,
name: "main".to_string(),
source: Some(super::types::Source {
name: Some(stopped_file.clone()),
path: Some(stopped_file.clone()),
}),
line: stopped_line as i64,
column: 0,
}];
let snapshot = ExecutionSnapshot {
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("internal error")
.as_nanos() as u64,
sequence: self.snapshots.len(),
variables,
call_stack,
location: SourceLocation {
file: stopped_file,
line: stopped_line,
column: Some(0),
},
delta: None,
};
if let Some(ref mut writer) = self.writer {
let recording_snapshot = Self::convert_to_recording_snapshot(&snapshot);
writer
.write_snapshot(&recording_snapshot)
.map_err(|e| format!("Failed to write snapshot to recording: {}", e))?;
}
self.snapshots.push(snapshot.clone());
Ok(snapshot)
}
/// Save recording to file (Sprint 72 JSON format - deprecated, use .pmat instead)
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "non_empty_index")]
pub fn save_to_file(&self, path: &str) -> Result<(), String> {
let json = serde_json::to_string_pretty(&self.snapshots)
.map_err(|e| format!("Failed to serialize: {}", e))?;
std::fs::write(path, json).map_err(|e| format!("Failed to write file: {}", e))?;
Ok(())
}
/// Start recording execution
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn start_recording(&mut self) {
self.is_recording = true;
}
/// Stop recording execution
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn stop_recording(&mut self) {
self.is_recording = false;
}
/// Check if currently recording
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn is_recording(&self) -> bool {
self.is_recording
}
/// Get the number of snapshots
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn snapshot_count(&self) -> usize {
self.snapshots.len()
}
}
impl ExecutionRecorder<std::io::Sink> {
/// Create a new memory-only execution recorder (Sprint 72 backward compatibility)
///
/// This maintains backward compatibility with existing code that doesn't need persistence
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new(dap_server: Arc<Mutex<DapServer>>) -> Self {
Self {
snapshots: Vec::new(),
is_recording: false,
dap_server,
writer: None,
}
}
/// Load recording from file (Sprint 72 JSON format)
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "non_empty_index")]
pub fn load_from_file(path: &str) -> Result<Self, String> {
let json =
std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
let snapshots: Vec<ExecutionSnapshot> =
serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize: {}", e))?;
// Create a dummy DAP server for loaded recordings
let dap_server = Arc::new(Mutex::new(DapServer::new()));
Ok(Self {
snapshots,
is_recording: false,
dap_server,
writer: None,
})
}
}
include!("execution_recorder_tests.rs");
include!("execution_recorder_tests_io.rs");