ruchy/runtime/transaction.rs
1//! Transactional state management for REPL evaluation
2//!
3//! Provides atomic evaluation with rollback capability for safe experimentation.
4use crate::runtime::interpreter::Value;
5use crate::runtime::safe_arena::{SafeArena as Arena, TransactionalArena};
6use anyhow::{anyhow, Result};
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9// ============================================================================
10// Transactional REPL State
11// ============================================================================
12/// Transaction ID for tracking evaluation transactions
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct TransactionId(pub u64);
15/// Transactional wrapper for REPL state with O(1) checkpoint/rollback
16pub struct TransactionalState {
17 /// Current bindings
18 bindings: HashMap<String, Value>,
19 /// Binding mutability tracking
20 binding_mutability: HashMap<String, bool>,
21 /// Transaction stack
22 transactions: Vec<Transaction>,
23 /// Next transaction ID
24 next_tx_id: u64,
25 /// Arena for memory allocation
26 arena: TransactionalArena,
27 /// Maximum transaction depth
28 max_depth: usize,
29}
30/// A single transaction in the stack
31#[derive(Debug, Clone)]
32struct Transaction {
33 id: TransactionId,
34 /// Snapshot of bindings at transaction start
35 bindings_snapshot: HashMap<String, Value>,
36 /// Snapshot of mutability at transaction start
37 mutability_snapshot: HashMap<String, bool>,
38 /// Arena checkpoint
39 arena_checkpoint: usize,
40 /// Start time for timeout tracking
41 start_time: Instant,
42 /// Transaction metadata
43 metadata: TransactionMetadata,
44}
45/// Metadata about a transaction
46#[derive(Debug, Clone)]
47pub struct TransactionMetadata {
48 /// Description of the transaction
49 pub description: String,
50 /// Memory limit for this transaction
51 pub memory_limit: Option<usize>,
52 /// Time limit for this transaction
53 pub time_limit: Option<Duration>,
54 /// Whether this is a speculative evaluation
55 pub speculative: bool,
56}
57impl Default for TransactionMetadata {
58 fn default() -> Self {
59 Self {
60 description: "evaluation".to_string(),
61 memory_limit: None,
62 time_limit: None,
63 speculative: false,
64 }
65 }
66}
67impl TransactionalState {
68 /// Create a new transactional state with the given memory limit
69 /// # Examples
70 ///
71 /// ```
72 /// use ruchy::runtime::transaction::TransactionalState;
73 ///
74 /// let instance = TransactionalState::new();
75 /// // Verify behavior
76 /// ```
77 /// # Examples
78 ///
79 /// ```
80 /// use ruchy::runtime::transaction::TransactionalState;
81 ///
82 /// let instance = TransactionalState::new();
83 /// // Verify behavior
84 /// ```
85 /// # Examples
86 ///
87 /// ```
88 /// use ruchy::runtime::transaction::TransactionalState;
89 ///
90 /// let instance = TransactionalState::new();
91 /// // Verify behavior
92 /// ```
93 pub fn new(max_memory: usize) -> Self {
94 Self {
95 bindings: HashMap::new(),
96 binding_mutability: HashMap::new(),
97 transactions: Vec::new(),
98 next_tx_id: 1,
99 arena: TransactionalArena::new(max_memory),
100 max_depth: 100, // Prevent unbounded nesting
101 }
102 }
103 /// Begin a new transaction
104 /// # Examples
105 ///
106 /// ```
107 /// use ruchy::runtime::transaction::TransactionalState;
108 ///
109 /// let mut instance = TransactionalState::new();
110 /// let result = instance.begin_transaction();
111 /// // Verify behavior
112 /// ```
113 pub fn begin_transaction(&mut self, metadata: TransactionMetadata) -> Result<TransactionId> {
114 if self.transactions.len() >= self.max_depth {
115 return Err(anyhow!("Transaction depth limit exceeded"));
116 }
117 let id = TransactionId(self.next_tx_id);
118 self.next_tx_id += 1;
119 // Create checkpoint in arena
120 let arena_checkpoint = self.arena.checkpoint();
121 // Create transaction with current state snapshot
122 let transaction = Transaction {
123 id,
124 bindings_snapshot: self.bindings.clone(),
125 mutability_snapshot: self.binding_mutability.clone(),
126 arena_checkpoint,
127 start_time: Instant::now(),
128 metadata,
129 };
130 self.transactions.push(transaction);
131 Ok(id)
132 }
133 /// Commit the current transaction
134 /// # Examples
135 ///
136 /// ```ignore
137 /// use ruchy::runtime::transaction::commit_transaction;
138 ///
139 /// let result = commit_transaction(());
140 /// assert_eq!(result, Ok(()));
141 /// ```
142 pub fn commit_transaction(&mut self, id: TransactionId) -> Result<()> {
143 let tx = self
144 .transactions
145 .last()
146 .ok_or_else(|| anyhow!("No active transaction"))?;
147 if tx.id != id {
148 return Err(anyhow!("Transaction ID mismatch"));
149 }
150 // Commit arena changes
151 self.arena.commit()?;
152 // Remove transaction from stack
153 self.transactions.pop();
154 Ok(())
155 }
156 /// Rollback the current transaction
157 /// # Examples
158 ///
159 /// ```ignore
160 /// use ruchy::runtime::transaction::rollback_transaction;
161 ///
162 /// let result = rollback_transaction(());
163 /// assert_eq!(result, Ok(()));
164 /// ```
165 pub fn rollback_transaction(&mut self, id: TransactionId) -> Result<()> {
166 let tx = self
167 .transactions
168 .last()
169 .ok_or_else(|| anyhow!("No active transaction"))?;
170 if tx.id != id {
171 return Err(anyhow!("Transaction ID mismatch"));
172 }
173 // Restore state from snapshot
174 let tx = self.transactions.pop().unwrap();
175 self.bindings = tx.bindings_snapshot;
176 self.binding_mutability = tx.mutability_snapshot;
177 // Rollback arena
178 self.arena.rollback(tx.arena_checkpoint)?;
179 Ok(())
180 }
181 /// Check if a transaction has exceeded its limits
182 /// # Examples
183 ///
184 /// ```ignore
185 /// use ruchy::runtime::transaction::check_transaction_limits;
186 ///
187 /// let result = check_transaction_limits(());
188 /// assert_eq!(result, Ok(()));
189 /// ```
190 pub fn check_transaction_limits(&self, id: TransactionId) -> Result<()> {
191 let tx = self
192 .transactions
193 .iter()
194 .find(|t| t.id == id)
195 .ok_or_else(|| anyhow!("Transaction not found"))?;
196 // Check time limit
197 if let Some(time_limit) = tx.metadata.time_limit {
198 if tx.start_time.elapsed() > time_limit {
199 return Err(anyhow!("Transaction time limit exceeded"));
200 }
201 }
202 // Check memory limit
203 if let Some(memory_limit) = tx.metadata.memory_limit {
204 if self.arena.arena().used() > memory_limit {
205 return Err(anyhow!("Transaction memory limit exceeded"));
206 }
207 }
208 Ok(())
209 }
210 /// Get current transaction depth
211 /// # Examples
212 ///
213 /// ```ignore
214 /// use ruchy::runtime::transaction::depth;
215 ///
216 /// let result = depth(());
217 /// assert_eq!(result, Ok(()));
218 /// ```
219 pub fn depth(&self) -> usize {
220 self.transactions.len()
221 }
222 /// Get current bindings
223 /// # Examples
224 ///
225 /// ```ignore
226 /// use ruchy::runtime::transaction::bindings;
227 ///
228 /// let result = bindings(());
229 /// assert_eq!(result, Ok(()));
230 /// ```
231 pub fn bindings(&self) -> &HashMap<String, Value> {
232 &self.bindings
233 }
234 /// Get mutable bindings
235 /// # Examples
236 ///
237 /// ```ignore
238 /// use ruchy::runtime::transaction::bindings_mut;
239 ///
240 /// let result = bindings_mut(());
241 /// assert_eq!(result, Ok(()));
242 /// ```
243 pub fn bindings_mut(&mut self) -> &mut HashMap<String, Value> {
244 &mut self.bindings
245 }
246 /// Insert a binding
247 /// # Examples
248 ///
249 /// ```ignore
250 /// use ruchy::runtime::transaction::insert_binding;
251 ///
252 /// let result = insert_binding(true);
253 /// assert_eq!(result, Ok(true));
254 /// ```
255 pub fn insert_binding(&mut self, name: String, value: Value, mutable: bool) {
256 self.bindings.insert(name.clone(), value);
257 self.binding_mutability.insert(name, mutable);
258 }
259 /// Get binding mutability
260 /// # Examples
261 ///
262 /// ```ignore
263 /// use ruchy::runtime::transaction::is_mutable;
264 ///
265 /// let result = is_mutable("example");
266 /// assert_eq!(result, Ok(()));
267 /// ```
268 pub fn is_mutable(&self, name: &str) -> bool {
269 self.binding_mutability.get(name).copied().unwrap_or(false)
270 }
271 /// Clear all bindings
272 /// # Examples
273 ///
274 /// ```ignore
275 /// use ruchy::runtime::transaction::clear;
276 ///
277 /// let result = clear(());
278 /// assert_eq!(result, Ok(()));
279 /// ```
280 /// # Examples
281 ///
282 /// ```ignore
283 /// use ruchy::runtime::transaction::clear;
284 ///
285 /// let result = clear(());
286 /// assert_eq!(result, Ok(()));
287 /// ```
288 pub fn clear(&mut self) {
289 self.bindings.clear();
290 self.binding_mutability.clear();
291 self.transactions.clear();
292 self.arena.reset();
293 }
294 /// Get arena for allocation
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use ruchy::runtime::transaction::TransactionalState;
300 ///
301 /// let state = TransactionalState::new();
302 /// let arena = state.arena();
303 /// assert!(arena.used() >= 0);
304 /// ```
305 pub fn arena(&self) -> &Arena {
306 self.arena.arena()
307 }
308 /// Get memory usage
309 ///
310 /// # Examples
311 ///
312 /// ```ignore
313 /// use ruchy::runtime::transaction::TransactionalState;
314 ///
315 /// let state = TransactionalState::new();
316 /// let used = state.memory_used();
317 /// assert!(used >= 0);
318 /// ```
319 pub fn memory_used(&self) -> usize {
320 self.arena.arena().used()
321 }
322 // SavePoint feature temporarily disabled - requires complex lifetime management
323 // /// Create a savepoint for nested transactions
324 //
325 /// # Examples
326 ///
327 /// ```ignore
328 /// use ruchy::runtime::transaction::savepoint;
329 ///
330 /// let result = savepoint(());
331 /// assert_eq!(result, Ok(()));
332 /// ```
333 pub fn savepoint(&mut self) -> Result<SavePoint> {
334 // SavePoint feature temporarily disabled - requires complex lifetime management
335 Err(anyhow!("SavePoint feature temporarily disabled"))
336 }
337}
338// ============================================================================
339// SavePoint - RAII Guard for Automatic Rollback
340// ============================================================================
341// SavePoint temporarily disabled - requires complex lifetime management
342// /// RAII guard for automatic transaction rollback
343// pub struct SavePoint {
344// tx_id: TransactionId,
345// state: Arc<RefCell<TransactionalState>>,
346// }
347// Placeholder for SavePoint
348pub struct SavePoint;
349// ============================================================================
350// Transaction Log for Debugging
351// ============================================================================
352/// Log entry for transaction events
353#[derive(Debug, Clone)]
354pub enum TransactionEvent {
355 Begin {
356 id: TransactionId,
357 metadata: TransactionMetadata,
358 },
359 Commit {
360 id: TransactionId,
361 duration: Duration,
362 memory_used: usize,
363 },
364 Rollback {
365 id: TransactionId,
366 reason: String,
367 },
368 BindingAdded {
369 name: String,
370 value_type: String,
371 },
372 BindingModified {
373 name: String,
374 old_type: String,
375 new_type: String,
376 },
377}
378/// Transaction log for debugging and analysis
379pub struct TransactionLog {
380 events: Vec<(Instant, TransactionEvent)>,
381 max_entries: usize,
382}
383impl TransactionLog {
384 pub fn new(max_entries: usize) -> Self {
385 Self {
386 events: Vec::new(),
387 max_entries,
388 }
389 }
390 /// # Examples
391 ///
392 /// ```
393 /// use ruchy::runtime::transaction::TransactionLog;
394 ///
395 /// let mut instance = TransactionLog::new();
396 /// let result = instance.log();
397 /// // Verify behavior
398 /// ```
399 pub fn log(&mut self, event: TransactionEvent) {
400 self.events.push((Instant::now(), event));
401 // Maintain size limit
402 if self.events.len() > self.max_entries {
403 self.events.remove(0);
404 }
405 }
406 /// # Examples
407 ///
408 /// ```ignore
409 /// use ruchy::runtime::transaction::recent_events;
410 ///
411 /// let result = recent_events(());
412 /// assert_eq!(result, Ok(()));
413 /// ```
414 pub fn recent_events(&self, count: usize) -> &[(Instant, TransactionEvent)] {
415 let start = self.events.len().saturating_sub(count);
416 &self.events[start..]
417 }
418 pub fn clear(&mut self) {
419 self.events.clear();
420 }
421}
422// ============================================================================
423// Optimistic Concurrency Control
424// ============================================================================
425/// Version number for optimistic concurrency control
426#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
427pub struct Version(u64);
428/// Versioned value for optimistic concurrency
429#[derive(Debug, Clone)]
430pub struct VersionedValue<T> {
431 pub value: T,
432 pub version: Version,
433}
434/// Multi-version concurrency control for parallel evaluation
435pub struct MVCC {
436 /// Current version
437 current_version: Version,
438 /// Versioned bindings
439 bindings: HashMap<String, Vec<VersionedValue<Value>>>,
440 /// Maximum versions to keep per binding
441 max_versions: usize,
442}
443impl MVCC {
444 pub fn new() -> Self {
445 Self {
446 current_version: Version(0),
447 bindings: HashMap::new(),
448 max_versions: 10,
449 }
450 }
451 /// Start a new read transaction
452 /// # Examples
453 ///
454 /// ```
455 /// use ruchy::runtime::transaction::MVCC;
456 ///
457 /// let mut instance = MVCC::new();
458 /// let result = instance.begin_read();
459 /// // Verify behavior
460 /// ```
461 pub fn begin_read(&self) -> Version {
462 self.current_version
463 }
464 /// Start a new write transaction
465 /// # Examples
466 ///
467 /// ```ignore
468 /// use ruchy::runtime::transaction::begin_write;
469 ///
470 /// let result = begin_write(());
471 /// assert_eq!(result, Ok(()));
472 /// ```
473 pub fn begin_write(&mut self) -> Version {
474 self.current_version.0 += 1;
475 self.current_version
476 }
477 /// Read a value at a specific version
478 /// # Examples
479 ///
480 /// ```ignore
481 /// use ruchy::runtime::transaction::read;
482 ///
483 /// let result = read("example");
484 /// assert_eq!(result, Ok(()));
485 /// ```
486 pub fn read(&self, name: &str, version: Version) -> Option<&Value> {
487 self.bindings.get(name).and_then(|versions| {
488 // Find the latest version <= requested version
489 versions
490 .iter()
491 .rev()
492 .find(|v| v.version <= version)
493 .map(|v| &v.value)
494 })
495 }
496 /// Write a value at a specific version
497 /// # Examples
498 ///
499 /// ```ignore
500 /// use ruchy::runtime::transaction::write;
501 ///
502 /// let result = write(());
503 /// assert_eq!(result, Ok(()));
504 /// ```
505 pub fn write(&mut self, name: String, value: Value, version: Version) {
506 let entry = self.bindings.entry(name).or_default();
507 // Add new version
508 entry.push(VersionedValue { value, version });
509 // Maintain version limit
510 if entry.len() > self.max_versions {
511 entry.remove(0);
512 }
513 }
514 /// Garbage collect old versions
515 /// # Examples
516 ///
517 /// ```ignore
518 /// use ruchy::runtime::transaction::gc;
519 ///
520 /// let result = gc(());
521 /// assert_eq!(result, Ok(()));
522 /// ```
523 pub fn gc(&mut self, keep_after: Version) {
524 for versions in self.bindings.values_mut() {
525 versions.retain(|v| v.version >= keep_after);
526 }
527 }
528}
529impl Default for MVCC {
530 fn default() -> Self {
531 Self::new()
532 }
533}
534#[cfg(test)]
535mod tests {
536 use super::*;
537 #[test]
538 fn test_transaction_commit() {
539 let mut state = TransactionalState::new(1024 * 1024);
540 // Add initial binding
541 state.insert_binding("x".to_string(), Value::Integer(1), false);
542 // Begin transaction
543 let tx = state
544 .begin_transaction(TransactionMetadata::default())
545 .unwrap();
546 // Modify binding
547 state.insert_binding("x".to_string(), Value::Integer(2), false);
548 state.insert_binding("y".to_string(), Value::Integer(3), false);
549 // Commit
550 state.commit_transaction(tx).unwrap();
551 // Changes should persist
552 assert_eq!(state.bindings.get("x"), Some(&Value::Integer(2)));
553 assert_eq!(state.bindings.get("y"), Some(&Value::Integer(3)));
554 }
555 #[test]
556 fn test_transaction_rollback() {
557 let mut state = TransactionalState::new(1024 * 1024);
558 // Add initial binding
559 state.insert_binding("x".to_string(), Value::Integer(1), false);
560 // Begin transaction
561 let tx = state
562 .begin_transaction(TransactionMetadata::default())
563 .unwrap();
564 // Modify binding
565 state.insert_binding("x".to_string(), Value::Integer(2), false);
566 state.insert_binding("y".to_string(), Value::Integer(3), false);
567 // Rollback
568 state.rollback_transaction(tx).unwrap();
569 // Changes should be reverted
570 assert_eq!(state.bindings.get("x"), Some(&Value::Integer(1)));
571 assert_eq!(state.bindings.get("y"), None);
572 }
573 // SavePoint test implementation
574 // #[test]
575 // fn test_savepoint() {
576 // let mut state = TransactionalState::new(1024 * 1024);
577 //
578 // state.insert_binding("x".to_string(), Value::Integer(1), false);
579 //
580 // {
581 // let sp = state.savepoint().unwrap();
582 // state.insert_binding("x".to_string(), Value::Integer(2), false);
583 // // SavePoint dropped here, automatic rollback
584 // }
585 //
586 // // Should be rolled back
587 // assert_eq!(state.bindings.get("x"), Some(&Value::Integer(1)));
588 // }
589 #[test]
590 fn test_mvcc() {
591 let mut mvcc = MVCC::new();
592 let v1 = mvcc.begin_write();
593 mvcc.write("x".to_string(), Value::Integer(1), v1);
594 let v2 = mvcc.begin_write();
595 mvcc.write("x".to_string(), Value::Integer(2), v2);
596 // Read at different versions
597 assert_eq!(mvcc.read("x", v1), Some(&Value::Integer(1)));
598 assert_eq!(mvcc.read("x", v2), Some(&Value::Integer(2)));
599 }
600}
601#[cfg(test)]
602mod property_tests_transaction {
603 use super::*;
604 use proptest::proptest;
605
606 proptest! {
607 /// Property: TransactionalState operations never panic
608 #[test]
609 fn test_transactional_state_never_panics(size: usize) {
610 let size = size % 10_000_000; // Limit memory size
611 let _ = TransactionalState::new(size);
612 }
613 /// Property: Transaction commit/rollback preserves invariants
614 #[test]
615 fn test_transaction_invariants(ops: Vec<u8>) {
616 let mut state = TransactionalState::new(1024 * 1024);
617 for op in ops.iter().take(100) {
618 match op % 3 {
619 0 => {
620 let _ = state.begin_transaction(TransactionMetadata::default());
621 },
622 1 => {
623 if state.depth() > 0 {
624 let tx = TransactionId(state.depth() as u64);
625 let _ = state.commit_transaction(tx);
626 }
627 },
628 _ => {
629 if state.depth() > 0 {
630 let tx = TransactionId(state.depth() as u64);
631 let _ = state.rollback_transaction(tx);
632 }
633 }
634 }
635 }
636 // Invariant: depth is always non-negative (usize type)
637 }
638 }
639}