Coverage Report

Created: 2025-09-05 15:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/runtime/transaction.rs
Line
Count
Source
1
//! Transactional state management for REPL evaluation
2
//!
3
//! Provides atomic evaluation with rollback capability for safe experimentation.
4
5
use anyhow::{Result, anyhow};
6
use std::collections::HashMap;
7
use std::time::{Duration, Instant};
8
9
use crate::runtime::repl::Value;
10
use crate::runtime::safe_arena::{TransactionalArena, SafeArena as Arena};
11
12
// ============================================================================
13
// Transactional REPL State
14
// ============================================================================
15
16
/// Transaction ID for tracking evaluation transactions
17
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18
pub struct TransactionId(pub u64);
19
20
/// Transactional wrapper for REPL state with O(1) checkpoint/rollback
21
pub struct TransactionalState {
22
    /// Current bindings
23
    bindings: HashMap<String, Value>,
24
    /// Binding mutability tracking
25
    binding_mutability: HashMap<String, bool>,
26
    /// Transaction stack
27
    transactions: Vec<Transaction>,
28
    /// Next transaction ID
29
    next_tx_id: u64,
30
    /// Arena for memory allocation
31
    arena: TransactionalArena,
32
    /// Maximum transaction depth
33
    max_depth: usize,
34
}
35
36
/// A single transaction in the stack
37
#[derive(Debug, Clone)]
38
struct Transaction {
39
    id: TransactionId,
40
    /// Snapshot of bindings at transaction start
41
    bindings_snapshot: HashMap<String, Value>,
42
    /// Snapshot of mutability at transaction start
43
    mutability_snapshot: HashMap<String, bool>,
44
    /// Arena checkpoint
45
    arena_checkpoint: usize,
46
    /// Start time for timeout tracking
47
    start_time: Instant,
48
    /// Transaction metadata
49
    metadata: TransactionMetadata,
50
}
51
52
/// Metadata about a transaction
53
#[derive(Debug, Clone)]
54
pub struct TransactionMetadata {
55
    /// Description of the transaction
56
    pub description: String,
57
    /// Memory limit for this transaction
58
    pub memory_limit: Option<usize>,
59
    /// Time limit for this transaction
60
    pub time_limit: Option<Duration>,
61
    /// Whether this is a speculative evaluation
62
    pub speculative: bool,
63
}
64
65
impl Default for TransactionMetadata {
66
2
    fn default() -> Self {
67
2
        Self {
68
2
            description: "evaluation".to_string(),
69
2
            memory_limit: None,
70
2
            time_limit: None,
71
2
            speculative: false,
72
2
        }
73
2
    }
74
}
75
76
impl TransactionalState {
77
    /// Create a new transactional state with the given memory limit
78
26
    pub fn new(max_memory: usize) -> Self {
79
26
        Self {
80
26
            bindings: HashMap::new(),
81
26
            binding_mutability: HashMap::new(),
82
26
            transactions: Vec::new(),
83
26
            next_tx_id: 1,
84
26
            arena: TransactionalArena::new(max_memory),
85
26
            max_depth: 100, // Prevent unbounded nesting
86
26
        }
87
26
    }
88
    
89
    /// Begin a new transaction
90
2
    pub fn begin_transaction(&mut self, metadata: TransactionMetadata) -> Result<TransactionId> {
91
2
        if self.transactions.len() >= self.max_depth {
92
0
            return Err(anyhow!("Transaction depth limit exceeded"));
93
2
        }
94
        
95
2
        let id = TransactionId(self.next_tx_id);
96
2
        self.next_tx_id += 1;
97
        
98
        // Create checkpoint in arena
99
2
        let arena_checkpoint = self.arena.checkpoint();
100
        
101
        // Create transaction with current state snapshot
102
2
        let transaction = Transaction {
103
2
            id,
104
2
            bindings_snapshot: self.bindings.clone(),
105
2
            mutability_snapshot: self.binding_mutability.clone(),
106
2
            arena_checkpoint,
107
2
            start_time: Instant::now(),
108
2
            metadata,
109
2
        };
110
        
111
2
        self.transactions.push(transaction);
112
2
        Ok(id)
113
2
    }
114
    
115
    /// Commit the current transaction
116
1
    pub fn commit_transaction(&mut self, id: TransactionId) -> Result<()> {
117
1
        let tx = self.transactions.last()
118
1
            .ok_or_else(|| anyhow!(
"No active transaction"0
))
?0
;
119
        
120
1
        if tx.id != id {
121
0
            return Err(anyhow!("Transaction ID mismatch"));
122
1
        }
123
        
124
        // Commit arena changes
125
1
        self.arena.commit()
?0
;
126
        
127
        // Remove transaction from stack
128
1
        self.transactions.pop();
129
        
130
1
        Ok(())
131
1
    }
132
    
133
    /// Rollback the current transaction
134
1
    pub fn rollback_transaction(&mut self, id: TransactionId) -> Result<()> {
135
1
        let tx = self.transactions.last()
136
1
            .ok_or_else(|| anyhow!(
"No active transaction"0
))
?0
;
137
        
138
1
        if tx.id != id {
139
0
            return Err(anyhow!("Transaction ID mismatch"));
140
1
        }
141
        
142
        // Restore state from snapshot
143
1
        let tx = self.transactions.pop().unwrap();
144
1
        self.bindings = tx.bindings_snapshot;
145
1
        self.binding_mutability = tx.mutability_snapshot;
146
        
147
        // Rollback arena
148
1
        self.arena.rollback(tx.arena_checkpoint)
?0
;
149
        
150
1
        Ok(())
151
1
    }
152
    
153
    /// Check if a transaction has exceeded its limits
154
0
    pub fn check_transaction_limits(&self, id: TransactionId) -> Result<()> {
155
0
        let tx = self.transactions.iter()
156
0
            .find(|t| t.id == id)
157
0
            .ok_or_else(|| anyhow!("Transaction not found"))?;
158
        
159
        // Check time limit
160
0
        if let Some(time_limit) = tx.metadata.time_limit {
161
0
            if tx.start_time.elapsed() > time_limit {
162
0
                return Err(anyhow!("Transaction time limit exceeded"));
163
0
            }
164
0
        }
165
        
166
        // Check memory limit
167
0
        if let Some(memory_limit) = tx.metadata.memory_limit {
168
0
            if self.arena.arena().used() > memory_limit {
169
0
                return Err(anyhow!("Transaction memory limit exceeded"));
170
0
            }
171
0
        }
172
        
173
0
        Ok(())
174
0
    }
175
    
176
    /// Get current transaction depth
177
0
    pub fn depth(&self) -> usize {
178
0
        self.transactions.len()
179
0
    }
180
    
181
    /// Get current bindings
182
0
    pub fn bindings(&self) -> &HashMap<String, Value> {
183
0
        &self.bindings
184
0
    }
185
    
186
    /// Get mutable bindings
187
0
    pub fn bindings_mut(&mut self) -> &mut HashMap<String, Value> {
188
0
        &mut self.bindings
189
0
    }
190
    
191
    /// Insert a binding
192
6
    pub fn insert_binding(&mut self, name: String, value: Value, mutable: bool) {
193
6
        self.bindings.insert(name.clone(), value);
194
6
        self.binding_mutability.insert(name, mutable);
195
6
    }
196
    
197
    /// Get binding mutability
198
0
    pub fn is_mutable(&self, name: &str) -> bool {
199
0
        self.binding_mutability.get(name).copied().unwrap_or(false)
200
0
    }
201
    
202
    /// Clear all bindings
203
0
    pub fn clear(&mut self) {
204
0
        self.bindings.clear();
205
0
        self.binding_mutability.clear();
206
0
        self.transactions.clear();
207
0
        self.arena.reset();
208
0
    }
209
    
210
    /// Get arena for allocation
211
0
    pub fn arena(&self) -> &Arena {
212
0
        self.arena.arena()
213
0
    }
214
    
215
    /// Get memory usage
216
0
    pub fn memory_used(&self) -> usize {
217
0
        self.arena.arena().used()
218
0
    }
219
    
220
    // SavePoint feature temporarily disabled - requires complex lifetime management
221
    // /// Create a savepoint for nested transactions
222
    // pub fn savepoint(&mut self) -> Result<SavePoint> {
223
    //     let tx_id = self.begin_transaction(TransactionMetadata {
224
    //         description: "savepoint".to_string(),
225
    //         speculative: true,
226
    //         ..Default::default()
227
    //     })?;
228
    //     
229
    //     Ok(SavePoint {
230
    //         tx_id,
231
    //         state: Rc::new(RefCell::new(*self)),
232
    //     })
233
    // }
234
}
235
236
// ============================================================================
237
// SavePoint - RAII Guard for Automatic Rollback
238
// ============================================================================
239
240
// SavePoint temporarily disabled - requires complex lifetime management
241
// /// RAII guard for automatic transaction rollback
242
// pub struct SavePoint {
243
//     tx_id: TransactionId,
244
//     state: Rc<RefCell<TransactionalState>>,
245
// }
246
247
// Placeholder for SavePoint
248
pub struct SavePoint;
249
250
// ============================================================================
251
// Transaction Log for Debugging
252
// ============================================================================
253
254
/// Log entry for transaction events
255
#[derive(Debug, Clone)]
256
pub enum TransactionEvent {
257
    Begin {
258
        id: TransactionId,
259
        metadata: TransactionMetadata,
260
    },
261
    Commit {
262
        id: TransactionId,
263
        duration: Duration,
264
        memory_used: usize,
265
    },
266
    Rollback {
267
        id: TransactionId,
268
        reason: String,
269
    },
270
    BindingAdded {
271
        name: String,
272
        value_type: String,
273
    },
274
    BindingModified {
275
        name: String,
276
        old_type: String,
277
        new_type: String,
278
    },
279
}
280
281
/// Transaction log for debugging and analysis
282
pub struct TransactionLog {
283
    events: Vec<(Instant, TransactionEvent)>,
284
    max_entries: usize,
285
}
286
287
impl TransactionLog {
288
0
    pub fn new(max_entries: usize) -> Self {
289
0
        Self {
290
0
            events: Vec::new(),
291
0
            max_entries,
292
0
        }
293
0
    }
294
    
295
0
    pub fn log(&mut self, event: TransactionEvent) {
296
0
        self.events.push((Instant::now(), event));
297
        
298
        // Maintain size limit
299
0
        if self.events.len() > self.max_entries {
300
0
            self.events.remove(0);
301
0
        }
302
0
    }
303
    
304
0
    pub fn recent_events(&self, count: usize) -> &[(Instant, TransactionEvent)] {
305
0
        let start = self.events.len().saturating_sub(count);
306
0
        &self.events[start..]
307
0
    }
308
    
309
0
    pub fn clear(&mut self) {
310
0
        self.events.clear();
311
0
    }
312
}
313
314
// ============================================================================
315
// Optimistic Concurrency Control
316
// ============================================================================
317
318
/// Version number for optimistic concurrency control
319
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
320
pub struct Version(u64);
321
322
/// Versioned value for optimistic concurrency
323
#[derive(Debug, Clone)]
324
pub struct VersionedValue<T> {
325
    pub value: T,
326
    pub version: Version,
327
}
328
329
/// Multi-version concurrency control for parallel evaluation
330
pub struct MVCC {
331
    /// Current version
332
    current_version: Version,
333
    /// Versioned bindings
334
    bindings: HashMap<String, Vec<VersionedValue<Value>>>,
335
    /// Maximum versions to keep per binding
336
    max_versions: usize,
337
}
338
339
impl MVCC {
340
1
    pub fn new() -> Self {
341
1
        Self {
342
1
            current_version: Version(0),
343
1
            bindings: HashMap::new(),
344
1
            max_versions: 10,
345
1
        }
346
1
    }
347
    
348
    /// Start a new read transaction
349
0
    pub fn begin_read(&self) -> Version {
350
0
        self.current_version
351
0
    }
352
    
353
    /// Start a new write transaction
354
2
    pub fn begin_write(&mut self) -> Version {
355
2
        self.current_version.0 += 1;
356
2
        self.current_version
357
2
    }
358
    
359
    /// Read a value at a specific version
360
2
    pub fn read(&self, name: &str, version: Version) -> Option<&Value> {
361
2
        self.bindings.get(name).and_then(|versions| {
362
            // Find the latest version <= requested version
363
2
            versions.iter()
364
2
                .rev()
365
3
                .
find2
(|v| v.version <= version)
366
2
                .map(|v| &v.value)
367
2
        })
368
2
    }
369
    
370
    /// Write a value at a specific version
371
2
    pub fn write(&mut self, name: String, value: Value, version: Version) {
372
2
        let entry = self.bindings.entry(name).or_default();
373
        
374
        // Add new version
375
2
        entry.push(VersionedValue { value, version });
376
        
377
        // Maintain version limit
378
2
        if entry.len() > self.max_versions {
379
0
            entry.remove(0);
380
2
        }
381
2
    }
382
    
383
    /// Garbage collect old versions
384
0
    pub fn gc(&mut self, keep_after: Version) {
385
0
        for versions in self.bindings.values_mut() {
386
0
            versions.retain(|v| v.version >= keep_after);
387
        }
388
0
    }
389
}
390
391
impl Default for MVCC {
392
0
    fn default() -> Self {
393
0
        Self::new()
394
0
    }
395
}
396
397
#[cfg(test)]
398
mod tests {
399
    use super::*;
400
    
401
    #[test]
402
1
    fn test_transaction_commit() {
403
1
        let mut state = TransactionalState::new(1024 * 1024);
404
        
405
        // Add initial binding
406
1
        state.insert_binding("x".to_string(), Value::Int(1), false);
407
        
408
        // Begin transaction
409
1
        let tx = state.begin_transaction(TransactionMetadata::default()).unwrap();
410
        
411
        // Modify binding
412
1
        state.insert_binding("x".to_string(), Value::Int(2), false);
413
1
        state.insert_binding("y".to_string(), Value::Int(3), false);
414
        
415
        // Commit
416
1
        state.commit_transaction(tx).unwrap();
417
        
418
        // Changes should persist
419
1
        assert_eq!(state.bindings.get("x"), Some(&Value::Int(2)));
420
1
        assert_eq!(state.bindings.get("y"), Some(&Value::Int(3)));
421
1
    }
422
    
423
    #[test]
424
1
    fn test_transaction_rollback() {
425
1
        let mut state = TransactionalState::new(1024 * 1024);
426
        
427
        // Add initial binding
428
1
        state.insert_binding("x".to_string(), Value::Int(1), false);
429
        
430
        // Begin transaction
431
1
        let tx = state.begin_transaction(TransactionMetadata::default()).unwrap();
432
        
433
        // Modify binding
434
1
        state.insert_binding("x".to_string(), Value::Int(2), false);
435
1
        state.insert_binding("y".to_string(), Value::Int(3), false);
436
        
437
        // Rollback
438
1
        state.rollback_transaction(tx).unwrap();
439
        
440
        // Changes should be reverted
441
1
        assert_eq!(state.bindings.get("x"), Some(&Value::Int(1)));
442
1
        assert_eq!(state.bindings.get("y"), None);
443
1
    }
444
    
445
    // SavePoint test disabled - feature temporarily disabled
446
    // #[test]
447
    // fn test_savepoint() {
448
    //     let mut state = TransactionalState::new(1024 * 1024);
449
    //     
450
    //     state.insert_binding("x".to_string(), Value::Int(1), false);
451
    //     
452
    //     {
453
    //         let sp = state.savepoint().unwrap();
454
    //         state.insert_binding("x".to_string(), Value::Int(2), false);
455
    //         // SavePoint dropped here, automatic rollback
456
    //     }
457
    //     
458
    //     // Should be rolled back
459
    //     assert_eq!(state.bindings.get("x"), Some(&Value::Int(1)));
460
    // }
461
    
462
    #[test]
463
1
    fn test_mvcc() {
464
1
        let mut mvcc = MVCC::new();
465
        
466
1
        let v1 = mvcc.begin_write();
467
1
        mvcc.write("x".to_string(), Value::Int(1), v1);
468
        
469
1
        let v2 = mvcc.begin_write();
470
1
        mvcc.write("x".to_string(), Value::Int(2), v2);
471
        
472
        // Read at different versions
473
1
        assert_eq!(mvcc.read("x", v1), Some(&Value::Int(1)));
474
1
        assert_eq!(mvcc.read("x", v2), Some(&Value::Int(2)));
475
1
    }
476
}