rusty_emu/plans/phase5_clock_migration.md
Jan-Peter von Hunnius 08f53d68fe feat: implement virtual clock timing architecture (Phases 5-8)
Introduces a VirtualClock-based timing system replacing wall-clock
Instant-based timing for hardware-accurate simulation.

New modules:
- virtual_clock.rs: Shared virtual time source with condvar sync
- real_time_governor.rs: Thread that advances virtual time
- component_timing.rs: Per-component timing constraints

Key changes:
- Component trait: added update_at_time(), timing(), set_virtual_clock_optional()
- All Intel 400x components migrated to virtual-time-aware operation
- ConfigurableSystem: start() returns Result, spawns governor thread
- GUI: timing controls (speed slider, mode selector, step button)
- Console: speed/mode/step/time commands
- Backward compatible: legacy fallback for components without VirtualClock

Reviewed-by: code review found 2 warnings (clock resume() dead code,
reset() safety docs) — both fixed.
2026-05-14 21:00:22 +02:00

29 KiB

Phase 5: Migrate Clock Components to Virtual Time

Overview

Phase 5 replaces Instant::now() and thread::sleep() in both clock components with virtual time from Arc<VirtualClock>. This is the first phase that modifies existing components rather than adding new infrastructure.

Files modified:

Files NOT modified (Phase 7 responsibility):


Implementation Order

Migrate GenericClock first, then TwoPhaseClock.

Rationale:

  1. GenericClock is simpler — single-phase, single CLK output
  2. TwoPhaseClock has dual outputs (PHI1/PHI2) and inter-phase dependencies
  3. Both follow the same pattern, so GenericClock establishes the template
  4. GenericClock has existing unit tests to validate the migration

Part A: GenericClock Migration

A1. Current Timing Mechanism

GenericClock uses wall-clock time in 5 locations:

Location Line Current Code Purpose
last_transition field 16 last_transition: Instant Track when last phase change occurred
pause_start field 21 pause_start: Option<Instant> Track when pause began
new() constructor 34 last_transition: Instant::now() Initialize transition timer
enable() 68 self.last_transition = Instant::now() Reset timer on enable
pause() 80 self.pause_start = Some(Instant::now()) Record pause start
resume() 88 pause_start.elapsed() Calculate pause duration
should_transition() 131 self.last_transition.elapsed() Check if phase duration elapsed
perform_transition() 149 self.last_transition = Instant::now() Record transition time
run() 205 thread::sleep(sleep_time) Busy-wait loop with sleep

A2. Required Changes

A2.1 — Add VirtualClock field (optional, gradual integration)

Replace Instant-based fields with virtual-time equivalents. Add Option<Arc<VirtualClock>> for gradual integration — clocks work without a VirtualClock (falling back to wall-clock), but use virtual time when one is injected.

// BEFORE (line 11-22):
pub struct GenericClock {
    base: BaseComponent,
    frequency: f64,
    duty_cycle: f64,
    current_state: PinValue,
    last_transition: Instant,
    high_time: Duration,
    low_time: Duration,
    enabled: bool,
    paused: bool,
    pause_start: Option<Instant>,
}

// AFTER:
pub struct GenericClock {
    base: BaseComponent,
    frequency: f64,
    duty_cycle: f64,
    current_state: PinValue,
    last_transition_vtime: Duration,       // Virtual time of last transition
    high_time: Duration,
    low_time: Duration,
    enabled: bool,
    paused: bool,
    pause_start_vtime: Option<Duration>,   // Virtual time when pause started
    virtual_clock: Option<Arc<VirtualClock>>,  // Injected in Phase 7
}

Key change: last_transition: Instant → last_transition_vtime: Duration (virtual nanoseconds since sim start). pause_start: Option<Instant> → pause_start_vtime: Option<Duration>.

A2.2 — Update constructor

// BEFORE (line 29-43):
let mut clock = GenericClock {
    base: BaseComponent::new(name, pins),
    frequency,
    duty_cycle: 0.5,
    current_state: PinValue::Low,
    last_transition: Instant::now(),
    high_time: Duration::from_secs_f64(0.5 / frequency),
    low_time: Duration::from_secs_f64(0.5 / frequency),
    enabled: true,
    paused: false,
    pause_start: None,
};

// AFTER:
let mut clock = GenericClock {
    base: BaseComponent::new(name, pins),
    frequency,
    duty_cycle: 0.5,
    current_state: PinValue::Low,
    last_transition_vtime: Duration::ZERO,  // Virtual time 0
    high_time: Duration::from_secs_f64(0.5 / frequency),
    low_time: Duration::from_secs_f64(0.5 / frequency),
    enabled: true,
    paused: false,
    pause_start_vtime: None,
    virtual_clock: None,  // Set later via set_virtual_clock()
};

A2.3 — Add set_virtual_clock() method

New method for Phase 7 injection:

/// Inject a VirtualClock for virtual-time-based operation.
/// If not set, the clock falls back to wall-clock Instant-based timing.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
    self.virtual_clock = Some(clock);
}

A2.4 — Update enable() method (line 64-69)

// BEFORE:
pub fn enable(&mut self) {
    self.enabled = true;
    self.current_state = PinValue::Low;
    self.last_transition = Instant::now();
}

// AFTER:
pub fn enable(&mut self) {
    self.enabled = true;
    self.current_state = PinValue::Low;
    self.last_transition_vtime = self.current_virtual_time();
}

Where current_virtual_time() is a new helper:

/// Get current virtual time, or fall back to Duration::ZERO if no VirtualClock.
fn current_virtual_time(&self) -> Duration {
    self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}

A2.5 — Update pause() and resume() (lines 78-94)

// BEFORE:
pub fn pause(&mut self) {
    self.paused = true;
    self.pause_start = Some(Instant::now());
}

pub fn resume(&mut self) {
    if let Some(pause_start) = self.pause_start {
        let pause_duration = pause_start.elapsed();
        self.last_transition += pause_duration;
    }
    self.paused = false;
    self.pause_start = None;
}

// AFTER:
pub fn pause(&mut self) {
    self.paused = true;
    self.pause_start_vtime = Some(self.current_virtual_time());
}

pub fn resume(&mut self) {
    if let Some(pause_start_vtime) = self.pause_start_vtime {
        let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime);
        self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration);
    }
    self.paused = false;
    self.pause_start_vtime = None;
}

A2.6 — Update should_transition() (line 130-138)

// BEFORE:
fn should_transition(&self) -> bool {
    let elapsed = self.last_transition.elapsed();
    match self.current_state {
        PinValue::High => elapsed >= self.high_time,
        PinValue::Low => elapsed >= self.low_time,
        PinValue::HighZ => true,
    }
}

// AFTER:
fn should_transition(&self, virtual_time: Duration) -> bool {
    let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
    match self.current_state {
        PinValue::High => elapsed >= self.high_time,
        PinValue::Low => elapsed >= self.low_time,
        PinValue::HighZ => true,
    }
}

Note: should_transition() now takes virtual_time: Duration as a parameter instead of computing elapsed from Instant.

A2.7 — Update perform_transition() (line 140-150)

// BEFORE:
fn perform_transition(&mut self) {
    let new_state = match self.current_state {
        PinValue::High => PinValue::Low,
        PinValue::Low => PinValue::High,
        PinValue::HighZ => PinValue::Low,
    };
    self.current_state = new_state;
    self.set_clock_output(new_state);
    self.last_transition = Instant::now();
}

// AFTER:
fn perform_transition(&mut self, virtual_time: Duration) {
    let new_state = match self.current_state {
        PinValue::High => PinValue::Low,
        PinValue::Low => PinValue::High,
        PinValue::HighZ => PinValue::Low,
    };
    self.current_state = new_state;
    self.set_clock_output(new_state);
    self.last_transition_vtime = virtual_time;
}

A2.8 — Implement update_at_time() (new, replaces update() for virtual-time path)

Add the virtual-time-aware update method. This overrides the default from the Component trait:

impl Component for GenericClock {
    // ... existing methods unchanged ...

    fn update_at_time(&mut self, virtual_time: Duration) {
        // Check pause state first - don't update if paused
        if self.paused {
            return;
        }

        // Read external enable control
        let external_enable = self.read_enable_pin();
        let should_be_enabled = self.enabled && external_enable;

        if !should_be_enabled {
            if self.current_state != PinValue::Low {
                self.set_clock_output(PinValue::Low);
                self.current_state = PinValue::Low;
            }
            return;
        }

        // Check if it's time to transition (using virtual time)
        if self.should_transition(virtual_time) {
            self.perform_transition(virtual_time);
        }
    }

    // Keep update() for backward compatibility — delegates to update_at_time
    // using current virtual time (or Duration::ZERO if no VirtualClock)
    fn update(&mut self) {
        let vtime = self.current_virtual_time();
        self.update_at_time(vtime);
    }
}

A2.9 — Rewrite run() to use wait_until() (line 190-210)

This is the most significant change. Replace the thread::sleep() busy-loop with virtual_clock.wait_until():

// BEFORE:
fn run(&mut self) {
    self.base.set_running(true);
    self.enable();

    while self.is_running() {
        self.update();
        let sleep_time = if self.frequency > 0.0 {
            Duration::from_secs_f64(1.0 / self.frequency / 100.0)
                .max(Duration::from_micros(1))
        } else {
            Duration::from_micros(100)
        };
        thread::sleep(sleep_time);
    }
    self.disable();
}

// AFTER:
fn run(&mut self) {
    self.base.set_running(true);
    self.enable();

    // If no VirtualClock is injected, fall back to wall-clock run loop
    let vclock = match self.virtual_clock.as_ref() {
        Some(vc) => Arc::clone(vc),
        None => {
            // Legacy path: use old Instant-based timing
            return self.run_legacy();
        }
    };

    while self.is_running() {
        // Calculate next transition time
        let next_transition = self.next_transition_time();

        // Block until virtual time reaches next transition
        match vclock.wait_until(next_transition) {
            Some(current_vtime) => {
                if !self.is_running() {
                    break;
                }
                self.update_at_time(current_vtime);
            }
            None => break, // Simulation stopped
        }
    }
    self.disable();
}

Add helper methods:

impl GenericClock {
    /// Calculate the virtual time of the next phase transition.
    fn next_transition_time(&self) -> Duration {
        let phase_duration = match self.current_state {
            PinValue::High => self.high_time,
            PinValue::Low => self.low_time,
            PinValue::HighZ => Duration::from_nanos(1),
        };
        self.last_transition_vtime.saturating_add(phase_duration)
    }

    /// Legacy run loop using wall-clock time (backward compatibility).
    fn run_legacy(&mut self) {
        while self.is_running() {
            self.update();
            let sleep_time = if self.frequency > 0.0 {
                Duration::from_secs_f64(1.0 / self.frequency / 100.0)
                    .max(Duration::from_micros(1))
            } else {
                Duration::from_micros(100)
            };
            thread::sleep(sleep_time);
        }
        self.disable();
    }
}

A2.10 — Add timing() override

impl Component for GenericClock {
    // ... existing methods ...

    fn timing(&self) -> ComponentTiming {
        ComponentTiming {
            name: "Generic Clock",
            access_time: Duration::ZERO,
            propagation_delay: Duration::ZERO,
            setup_time: Duration::ZERO,
            hold_time: Duration::ZERO,
            max_clock_frequency: f64::INFINITY,
            min_update_interval: self.high_time.min(self.low_time),
        }
    }
}

A2.11 — Update imports

// BEFORE (line 1-9):
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crate::component::{BaseComponent, Component};
use crate::pin::{Pin, PinValue};

// AFTER:
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;

Key change: Remove Instant from imports, add VirtualClock and ComponentTiming.

A3. GenericClock Test Impact

The existing tests in generic_clock.rs:238-258 are:

Test What it checks Impact
test_clock_creation Name and running state No impact — doesn't use Instant
test_clock_timing_calculation frequency_to_duration() No impact — static calculation

These tests will pass unchanged. No new tests needed for the migration itself — the existing tests validate construction and static calculations. Virtual-time behavior tests should be added in Phase 9.


Part B: TwoPhaseClock Migration

B1. Current Timing Mechanism

TwoPhaseClock uses wall-clock time in 5 locations:

Location Line Current Code Purpose
last_transition field 15 last_transition: Instant Track when last phase swap occurred
pause_start field 19 pause_start: Option<Instant> Track when pause began
new() constructor 38 last_transition: Instant::now() Initialize transition timer
enable() 64 self.last_transition = Instant::now() Reset timer on enable
pause() 80 self.pause_start = Some(Instant::now()) Record pause start
resume() 88 pause_start.elapsed() Calculate pause duration
should_transition() 137 self.last_transition.elapsed() Check if phase_time elapsed
perform_transition() 160 self.last_transition = Instant::now() Record transition time
run() 203 thread::sleep(Duration::from_micros(100)) Fixed 100µs busy-wait

B2. Required Changes

B2.1 — Add VirtualClock field (optional, gradual integration)

// BEFORE (line 11-21):
pub struct TwoPhaseClock {
    base: BaseComponent,
    phi1_state: PinValue,
    phi2_state: PinValue,
    last_transition: Instant,
    phase_time: Duration,
    enabled: bool,
    paused: bool,
    pause_start: Option<Instant>,
    frequency: f64,
}

// AFTER:
pub struct TwoPhaseClock {
    base: BaseComponent,
    phi1_state: PinValue,
    phi2_state: PinValue,
    last_transition_vtime: Duration,           // Virtual time of last phase swap
    phase_time: Duration,
    enabled: bool,
    paused: bool,
    pause_start_vtime: Option<Duration>,       // Virtual time when pause started
    frequency: f64,
    virtual_clock: Option<Arc<VirtualClock>>,  // Injected in Phase 7
}

B2.2 — Update constructor (line 24-45)

// BEFORE:
TwoPhaseClock {
    base: BaseComponent::new(name, pins),
    phi1_state: PinValue::High,
    phi2_state: PinValue::Low,
    last_transition: Instant::now(),
    phase_time,
    enabled: true,
    paused: false,
    pause_start: None,
    frequency,
}

// AFTER:
TwoPhaseClock {
    base: BaseComponent::new(name, pins),
    phi1_state: PinValue::High,
    phi2_state: PinValue::Low,
    last_transition_vtime: Duration::ZERO,
    phase_time,
    enabled: true,
    paused: false,
    pause_start_vtime: None,
    frequency,
    virtual_clock: None,
}

B2.3 — Add set_virtual_clock() and current_virtual_time() helpers

impl TwoPhaseClock {
    /// Inject a VirtualClock for virtual-time-based operation.
    pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
        self.virtual_clock = Some(clock);
    }

    /// Get current virtual time, or Duration::ZERO if no VirtualClock.
    fn current_virtual_time(&self) -> Duration {
        self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
    }
}

B2.4 — Update enable() (line 60-69)

// BEFORE:
pub fn enable(&mut self) {
    self.enabled = true;
    self.phi1_state = PinValue::High;
    self.phi2_state = PinValue::Low;
    self.last_transition = Instant::now();
    self.update_outputs();
    self.update_outputs();
}

// AFTER:
pub fn enable(&mut self) {
    self.enabled = true;
    self.phi1_state = PinValue::High;
    self.phi2_state = PinValue::Low;
    self.last_transition_vtime = self.current_virtual_time();
    self.update_outputs();
    self.update_outputs();
}

B2.5 — Update pause() and resume() (lines 79-95)

Same pattern as GenericClock:

pub fn pause(&mut self) {
    self.paused = true;
    self.pause_start_vtime = Some(self.current_virtual_time());
}

pub fn resume(&mut self) {
    if let Some(pause_start_vtime) = self.pause_start_vtime {
        let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime);
        self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration);
    }
    self.paused = false;
    self.pause_start_vtime = None;
}

B2.6 — Update should_transition() (line 136-138)

// BEFORE:
fn should_transition(&self) -> bool {
    self.last_transition.elapsed() >= self.phase_time
}

// AFTER:
fn should_transition(&self, virtual_time: Duration) -> bool {
    let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
    elapsed >= self.phase_time
}

B2.7 — Update perform_transition() (line 140-161)

// BEFORE:
fn perform_transition(&mut self) {
    match (self.phi1_state, self.phi2_state) {
        (PinValue::High, PinValue::Low) => {
            self.phi1_state = PinValue::Low;
            self.phi2_state = PinValue::High;
        }
        (PinValue::Low, PinValue::High) => {
            self.phi1_state = PinValue::High;
            self.phi2_state = PinValue::Low;
        }
        _ => {
            self.phi1_state = PinValue::High;
            self.phi2_state = PinValue::Low;
        }
    }
    self.update_outputs();
    self.last_transition = Instant::now();
}

// AFTER:
fn perform_transition(&mut self, virtual_time: Duration) {
    match (self.phi1_state, self.phi2_state) {
        (PinValue::High, PinValue::Low) => {
            self.phi1_state = PinValue::Low;
            self.phi2_state = PinValue::High;
        }
        (PinValue::Low, PinValue::High) => {
            self.phi1_state = PinValue::High;
            self.phi2_state = PinValue::Low;
        }
        _ => {
            self.phi1_state = PinValue::High;
            self.phi2_state = PinValue::Low;
        }
    }
    self.update_outputs();
    self.last_transition_vtime = virtual_time;
}

B2.8 — Implement update_at_time() and update update()

impl Component for TwoPhaseClock {
    // ... existing name/pins/get_pin unchanged ...

    fn update_at_time(&mut self, virtual_time: Duration) {
        if self.paused {
            return;
        }
        if !self.enabled {
            return;
        }
        self.update_outputs();
        if self.should_transition(virtual_time) {
            self.perform_transition(virtual_time);
        }
    }

    fn update(&mut self) {
        let vtime = self.current_virtual_time();
        self.update_at_time(vtime);
    }

    // ... rest unchanged ...
}

B2.9 — Rewrite run() to use wait_until() (line 195-212)

// BEFORE:
fn run(&mut self) {
    self.base.set_running(true);
    self.enable();
    while self.is_running() {
        self.update();
        thread::sleep(Duration::from_micros(100));
        if !self.is_running() {
            break;
        }
    }
    self.disable();
}

// AFTER:
fn run(&mut self) {
    self.base.set_running(true);
    self.enable();

    let vclock = match self.virtual_clock.as_ref() {
        Some(vc) => Arc::clone(vc),
        None => return self.run_legacy(),
    };

    while self.is_running() {
        let next_transition = self.last_transition_vtime + self.phase_time;
        match vclock.wait_until(next_transition) {
            Some(current_vtime) => {
                if !self.is_running() {
                    break;
                }
                self.update_at_time(current_vtime);
            }
            None => break,
        }
    }
    self.disable();
}

Add legacy helper:

impl TwoPhaseClock {
    /// Legacy run loop using wall-clock time (backward compatibility).
    fn run_legacy(&mut self) {
        while self.is_running() {
            self.update();
            thread::sleep(Duration::from_micros(100));
            if !self.is_running() {
                break;
            }
        }
        self.disable();
    }
}

B2.10 — Add timing() override

fn timing(&self) -> ComponentTiming {
    ComponentTiming {
        name: "Two-Phase Clock",
        access_time: Duration::ZERO,
        propagation_delay: Duration::ZERO,
        setup_time: Duration::ZERO,
        hold_time: Duration::ZERO,
        max_clock_frequency: f64::INFINITY,
        min_update_interval: self.phase_time,
    }
}

B2.11 — Update imports

// BEFORE (line 1-9):
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crate::component::{BaseComponent, Component};
use crate::pin::{Pin, PinValue};

// AFTER:
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;

B3. TwoPhaseClock Test Impact

No tests exist for TwoPhaseClock (no #[cfg(test)] module in the file). No test breakage risk.


Part C: System Config Changes

C1. Factory Closure Updates

In system_config.rs, the factory closures create clocks without a VirtualClock. In Phase 5, these remain unchanged — the virtual_clock field is None by default, and clocks use the legacy run_legacy() path.

In Phase 7, ConfigurableSystem will:

  1. Create Arc<VirtualClock>
  2. After constructing each clock, call set_virtual_clock(Arc::clone(&virtual_clock))
  3. This requires changing the factory closure signature or adding a post-construction hook

For Phase 5, no changes to system_config.rs are needed.


Part D: Backward Compatibility Analysis

D1. What stays the same

Aspect Compatibility
new(name, frequency) constructor signature Unchanged — no new required parameters
Component trait impl Unchanged — update() still works via delegation
enable() / disable() / pause() / resume() Unchanged — public API identical
set_frequency() / get_frequency() Unchanged
Pin names (CLK, PHI1, PHI2, ENABLE) Unchanged
run() without VirtualClock Works — falls back to run_legacy() with thread::sleep()
Existing tests Pass — no Instant dependency in test assertions

D2. What changes (internal only)

Change Visible externally?
last_transition: Instant → last_transition_vtime: Duration No — private field
pause_start: Option<Instant> → pause_start_vtime: Option<Duration> No — private field
should_transition() takes Duration param No — private method
perform_transition() takes Duration param No — private method
New virtual_clock: Option<Arc<VirtualClock>> field No — private, None by default
New set_virtual_clock() method Yes — but opt-in, not required

D3. Risk assessment

Risk: LOW. All changes are internal field replacements. The public API is unchanged. The Option<Arc<VirtualClock>> pattern ensures clocks work identically without a VirtualClock injected.


Part E: Execution Checklist

These are the implementation steps in order:

  • E1. Update GenericClock imports — remove Instant, add VirtualClock + ComponentTiming
  • E2. Replace GenericClock struct fields: last_transition: Instant → last_transition_vtime: Duration, pause_start: Option<Instant> → pause_start_vtime: Option<Duration>, add virtual_clock: Option<Arc<VirtualClock>>
  • E3. Update GenericClock::new() — initialize last_transition_vtime: Duration::ZERO, virtual_clock: None
  • E4. Add GenericClock::set_virtual_clock() and GenericClock::current_virtual_time() helper methods
  • E5. Update GenericClock::enable() — use current_virtual_time() instead of Instant::now()
  • E6. Update GenericClock::pause() — use current_virtual_time() instead of Instant::now()
  • E7. Update GenericClock::resume() — compute pause duration from virtual time deltas
  • E8. Update GenericClock::should_transition() — accept virtual_time: Duration parameter
  • E9. Update GenericClock::perform_transition() — accept virtual_time: Duration, store in last_transition_vtime
  • E10. Implement GenericClock::update_at_time() in Component impl
  • E11. Update GenericClock::update() to delegate to update_at_time(current_virtual_time())
  • E12. Add GenericClock::next_transition_time() and GenericClock::run_legacy() helper methods
  • E13. Rewrite GenericClock::run() — use wait_until() when VirtualClock present, else run_legacy()
  • E14. Add GenericClock::timing() override in Component impl
  • E15. Verify GenericClock existing tests still pass
  • E16. Update TwoPhaseClock imports — remove Instant, add VirtualClock + ComponentTiming
  • E17. Replace TwoPhaseClock struct fields: same pattern as GenericClock
  • E18. Update TwoPhaseClock::new() — initialize virtual time fields
  • E19. Add TwoPhaseClock::set_virtual_clock() and TwoPhaseClock::current_virtual_time() helpers
  • E20. Update TwoPhaseClock::enable() — use current_virtual_time()
  • E21. Update TwoPhaseClock::pause() and resume() — virtual time deltas
  • E22. Update TwoPhaseClock::should_transition() — accept virtual_time: Duration
  • E23. Update TwoPhaseClock::perform_transition() — accept virtual_time: Duration
  • E24. Implement TwoPhaseClock::update_at_time() in Component impl
  • E25. Update TwoPhaseClock::update() to delegate to update_at_time()
  • E26. Add TwoPhaseClock::run_legacy() helper
  • E27. Rewrite TwoPhaseClock::run() — use wait_until() when VirtualClock present
  • E28. Add TwoPhaseClock::timing() override in Component impl
  • E29. Run cargo build to verify compilation
  • E30. Run cargo test to verify all existing tests pass

Flow Diagram: Clock Thread with Virtual Time

flowchart TD
    A[run called] --> B{virtual_clock set?}
    B -->|No| C[run_legacy - thread::sleep loop]
    B -->|Yes| D[enable - reset phase state]
    D --> E[Calculate next_transition_time]
    E --> F[wait_until next_transition]
    F --> G{wait_until returned}
    G -->|None - stopped| H[disable and exit]
    G -->|Some vtime| I{is_running?}
    I -->|No| H
    I -->|Yes| J[update_at_time with vtime]
    J --> K[Transition phase if needed]
    K --> E

Flow Diagram: Phase Transition Logic in Virtual Time

flowchart LR
    subgraph GenericClock
        G1[last_transition_vtime = 0ns] --> G2{vtime >= high_time?}
        G2 -->|Yes| G3[High to Low transition]
        G3 --> G4[last_transition_vtime = vtime]
        G4 --> G5{vtime >= low_time?}
        G5 -->|Yes| G6[Low to High transition]
        G6 --> G4
    end

    subgraph TwoPhaseClock
        T1[last_transition_vtime = 0ns] --> T2{elapsed >= phase_time?}
        T2 -->|Yes| T3[PHI1 High+PHI2 Low to PHI1 Low+PHI2 High]
        T3 --> T4[last_transition_vtime = vtime]
        T4 --> T5{elapsed >= phase_time?}
        T5 -->|Yes| T6[PHI1 Low+PHI2 High to PHI1 High+PHI2 Low]
        T6 --> T4
    end