rusty_emu/plans/phase6_timing_enforcement.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

30 KiB

Phase 6: Add Timing Enforcement to Memory/CPU Components

Overview

Phase 6 adds virtual-time-based timing enforcement to the four Intel 400x series components: ROM (4001), RAM (4002), Shift Register (4003), and CPU (4004). These components currently use Instant::now() for address latch timestamps and check latency via Instant::elapsed(). This phase replaces those with virtual time from Arc<VirtualClock>, adds ComponentTiming enforcement checks, and records timing violations.

Files modified:

Files NOT modified (Phase 7 responsibility):


Implementation Order

graph LR
    A[Step 0: intel_400x.rs shared code] --> B[Step 1: Intel 4003 SR]
    B --> C[Step 2: Intel 4001 ROM]
    C --> D[Step 3: Intel 4002 RAM]
    D --> E[Step 4: Intel 4004 CPU]

Rationale:

  1. intel_400x.rs first — The shared state machine contains address_latch_time: Option<Instant> and is_latency_elapsed() used by all three memory components. Changing this first unblocks all component migrations.
  2. Intel 4003 second — Simplest component; no address latching in the traditional sense, uses shift/load operations. Good validation of the shared code changes.
  3. Intel 4001 third — ROM is simpler than RAM (read-only, no write operations). Validates the full address→wait→drive flow.
  4. Intel 4002 fourth — RAM adds write operations and I/O port states on top of the ROM pattern.
  5. Intel 4004 last — CPU is the most complex (98K chars, own MemoryState enum, own address_latch_time field). Benefits from all patterns established by the memory components.

Step 0: Migrate Shared State Machine (intel_400x.rs)

Current Timing Mechanism

The shared Intel400xStateMachine uses Instant in two critical locations:

Location Line Current Code Purpose
address_latch_time field 824 address_latch_time: Option<Instant> Timestamp when address was fully latched
handle_address_nibble() 958 self.address_latch_time = Some(Instant::now()) Record latch time when both nibbles received
is_latency_elapsed() 969 latch_time.elapsed() >= self.access_time Check if access time has passed

Additionally, the Intel400xTimingState trait exposes Instant in its interface:

Location Line Signature
get_address_latch_time() 324 fn get_address_latch_time(&self) -> Option<Instant>
set_address_latch_time() 325 fn set_address_latch_time(&mut self, time: Option<Instant>)

And the Intel400xAddressHandling trait's handle_address_latching() method takes &mut Option<Instant>:

Location Line Current Code
handle_address_latching() 212 address_latch_time: &mut Option<Instant> parameter
223 *address_latch_time = Some(Instant::now())
handle_latency_wait() 236 address_latch_time: &Option<Instant> parameter
240 latch_time.elapsed() >= access_time

Required Changes

0.1 — Change address_latch_time from Instant to Duration

Replace Instant-based timing with virtual time (Duration = nanoseconds since simulation start).

// BEFORE (line 824):
address_latch_time: Option<Instant>,

// AFTER:
address_latch_time: Option<Duration>,  // Virtual time when address was latched

0.2 — Add is_latency_elapsed_at() method (new, virtual-time-aware)

Add a new method that takes the current virtual time as a parameter, replacing the Instant::elapsed()-based check:

// NEW METHOD — add after is_latency_elapsed() at line ~973:
/// Check if latency has elapsed at a given virtual time.
/// Returns true if `virtual_time - address_latch_time >= access_time`.
pub fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
    if let Some(latch_time) = self.address_latch_time {
        virtual_time.saturating_sub(latch_time) >= self.access_time
    } else {
        false
    }
}

Keep the existing is_latency_elapsed() method for backward compatibility — it will continue to work with Instant::now() for legacy code paths. Mark it with a comment that it's deprecated for virtual-time usage.

0.3 — Add set_address_latch_vtime() method (new)

/// Set address latch time using virtual time directly.
/// Called from update_at_time() when both address nibbles are latched.
pub fn set_address_latch_vtime(&mut self, vtime: Duration) {
    self.address_latch_time = Some(vtime);
}

0.4 — Add handle_address_nibble_at() method (new, virtual-time-aware)

/// Handle address nibble latching with virtual time.
/// Returns true only when BOTH nibbles have been latched.
/// Records address_latch_time as virtual_time when complete.
pub fn handle_address_nibble_at(&mut self, nibble: u8, virtual_time: Duration) -> bool {
    if self.address_high_nibble.is_none() {
        self.address_high_nibble = Some(nibble);
        false
    } else if self.address_low_nibble.is_none() {
        self.address_low_nibble = Some(nibble);
        self.address_latch_time = Some(virtual_time);  // Virtual time, not Instant::now()
        true
    } else {
        false
    }
}

0.5 — Update Intel400xTimingState trait (additive only)

Add new virtual-time-aware methods to the trait alongside existing ones:

pub trait Intel400xTimingState {
    // ... existing methods unchanged ...

    /// Get address latch time as virtual time (Duration)
    fn get_address_latch_vtime(&self) -> Option<Duration>;

    /// Set address latch time using virtual time
    fn set_address_latch_vtime(&mut self, time: Option<Duration>);

    /// Check if latency has elapsed at a given virtual time
    fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool;
}

0.6 — Update Intel400xAddressHandling trait (additive only)

Add virtual-time-aware versions alongside existing methods:

pub trait Intel400xAddressHandling {
    // ... existing methods unchanged ...

    /// Handle latency wait check using virtual time
    fn handle_latency_wait_at(
        &self,
        address_latch_time: &Option<Duration>,
        access_time: Duration,
        virtual_time: Duration,
    ) -> bool {
        if let Some(latch_time) = address_latch_time {
            virtual_time.saturating_sub(*latch_time) >= access_time
        } else {
            false
        }
    }
}

0.7 — Update getter/setter for address_latch_time

// Change signatures (lines 916-923):
pub fn get_address_latch_time(&self) -> Option<Duration> {  // was Option<Instant>
    self.address_latch_time
}

pub fn set_address_latch_time(&mut self, time: Option<Duration>) {  // was Option<Instant>
    self.address_latch_time = time;
}

Impact: This changes the return type of get_address_latch_time() from Option<Instant> to Option<Duration>. All callers (the Intel400xTimingState impls in each component) must be updated to match.


Step 1: Intel 4003 Shift Register

Current Timing Mechanism

Intel4003 is the simplest component:

  • Uses Intel400xStateMachine for state management
  • Implements Intel400xTimingState trait
  • Has no direct Instant::now() calls — all timing goes through the shared state machine
  • Has thread::sleep(Duration::from_micros(1)) in run() (line 597)
  • Uses prev_phi1/prev_phi2 for edge detection (same as ROM/RAM)

Timing Enforcement Points

The 4003 has simpler timing than ROM/RAM:

  • No address latching — responds_to_address() = false
  • Shift operations triggered by clock edges
  • Access time: 200ns — faster than ROM/RAM

Required Changes

1.1 — Add VirtualClock field and helper

// Add to struct Intel4003 (after state_machine field):
virtual_clock: Option<Arc<VirtualClock>>,

// Add helper method:
fn current_virtual_time(&self) -> Duration {
    self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}

1.2 — Add set_virtual_clock() method

pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
    self.virtual_clock = Some(clock);
}

1.3 — Update Intel400xTimingState impl

Change get_address_latch_time / set_address_latch_time to use Duration:

// BEFORE (line 124):
fn get_address_latch_time(&self) -> Option<Instant> {
    self.state_machine.get_address_latch_time()
}

fn set_address_latch_time(&mut self, time: Option<Instant>) {
    self.state_machine.set_address_latch_time(time);
}

// AFTER:
fn get_address_latch_time(&self) -> Option<Duration> {
    self.state_machine.get_address_latch_time()
}

fn set_address_latch_time(&mut self, time: Option<Duration>) {
    self.state_machine.set_address_latch_time(time);
}

// New virtual-time methods:
fn get_address_latch_vtime(&self) -> Option<Duration> {
    self.state_machine.get_address_latch_time()
}

fn set_address_latch_vtime(&mut self, time: Option<Duration>) {
    self.state_machine.set_address_latch_time(time);
}

fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
    self.state_machine.is_latency_elapsed_at(virtual_time)
}

1.4 — Implement update_at_time()

fn update_at_time(&mut self, virtual_time: Duration) {
    if !self.is_running() {
        return;
    }

    let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1);
    let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1);
    let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2);
    let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2);

    let (phi1, phi2) = self.read_clock_pins();
    self.prev_phi1 = phi1;
    self.prev_phi2 = phi2;

    let reset_active = self.read_reset_pin();
    if reset_active {
        self.perform_reset();
        return;
    }

    // Timing enforcement: check if clock frequency is within spec
    // (frequency check happens at clock edge detection time)

    if phi2_falling {
        self.handle_phi2_falling();
    }
    if phi1_falling {
        self.handle_phi1_falling();
    }
    if phi1_rising {
        self.handle_phi1_rising();
    }
    if phi2_rising {
        self.handle_phi2_rising();
    }
}

1.5 — Update update() to delegate to update_at_time()

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

1.6 — Update run() with VirtualClock wait pattern

fn run(&mut self) {
    self.base.set_running(true);

    // Initialize clock states for edge detection
    let (phi1, phi2) = self.read_clock_pins();
    self.prev_phi1 = phi1;
    self.prev_phi2 = phi2;

    // If no VirtualClock, fall back to legacy busy-wait loop
    let vclock = match self.virtual_clock.as_ref() {
        Some(vc) => Arc::clone(vc),
        None => {
            while self.is_running() {
                self.update();
                thread::sleep(Duration::from_micros(1));
            }
            return;
        }
    };

    // Virtual-time run loop
    let timing = ComponentTiming::intel_4003();
    while self.is_running() {
        let next_update = vclock.now() + timing.min_update_interval;
        match vclock.wait_until(next_update) {
            Some(current_vtime) => {
                if !self.is_running() { break; }
                self.update_at_time(current_vtime);
            }
            None => break,
        }
    }
}

1.7 — Implement timing() trait method

fn timing(&self) -> ComponentTiming {
    ComponentTiming::intel_4003()
}

1.8 — Update constructor

// Add to new_with_access_time():
virtual_clock: None,

1.9 — Add import

use crate::virtual_clock::VirtualClock;
use crate::component_timing::ComponentTiming;

Step 2: Intel 4001 ROM

Current Timing Mechanism

Intel4001 uses timing in several places:

Location Line Current Code Purpose
access_time field 29 access_time: Duration ROM access latency (500ns)
state_machine.is_latency_elapsed() 277 In execute_state_logic() WaitLatency Check if ROM access time elapsed
state_machine.is_latency_elapsed() 782 In legacy handle_latency_wait() Same check in legacy path
thread::sleep() 968 In run() Busy-wait loop

The ROM state machine flow:

Idle → AddressPhase → WaitLatency → DriveData → Idle

Timing Enforcement Points

graph TD
    A[PHI1 Rising: Address Latching] -->|address_latch_time = virtual_time| B[WaitLatency State]
    B -->|Check: virtual_time - latch_time >= access_time| C{Access Time Elapsed?}
    C -->|Yes| D[PHI2 Rising: Drive Data onto bus]
    C -->|No - Timing Violation| E[Record violation - data undefined]
    D --> F[PHI2 Falling: Tri-state bus, return to Idle]

Key enforcement point: In WaitLatency state, when checking if latency has elapsed, use virtual time comparison. If the clock is overclocked (phase time < access_time), the ROM will NOT produce valid data — just like real hardware.

Required Changes

2.1 — Add VirtualClock field and helper

// Add to struct Intel4001:
virtual_clock: Option<Arc<VirtualClock>>,

// Add helper:
fn current_virtual_time(&self) -> Duration {
    self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}

2.2 — Add set_virtual_clock() method

pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
    self.virtual_clock = Some(clock);
}

2.3 — Update Intel400xTimingState impl

Same pattern as 4003 — change Instant to Duration in get/set_address_latch_time(), add new virtual-time trait methods.

2.4 — Implement update_at_time()

fn update_at_time(&mut self, virtual_time: Duration) {
    if !self.is_running() {
        return;
    }

    let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1);
    let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1);
    let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2);
    let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2);

    let (phi1, phi2) = self.read_clock_pins();
    self.prev_phi1 = phi1;
    self.prev_phi2 = phi2;

    let reset_active = self.read_reset_pin();
    if reset_active {
        self.perform_reset();
        return;
    }

    if phi2_falling { self.handle_phi2_falling(); }
    if phi1_falling { self.handle_phi1_falling(); }
    if phi1_rising { self.handle_phi1_rising(); }
    if phi2_rising { self.handle_phi2_rising(); }
}

2.5 — Update update() to delegate

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

2.6 — Update run() with VirtualClock wait pattern

Same pattern as 4003 — check for virtual_clock, fall back to legacy busy-wait if absent.

2.7 — Implement timing() trait method

fn timing(&self) -> ComponentTiming {
    ComponentTiming::intel_4001()
}

2.8 — Update handle_phi2_rising() for timing enforcement

The key timing enforcement happens in handle_phi2_rising() where WaitLatency state checks is_latency_elapsed():

// BEFORE (line 487-493):
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
    if self.a3_wait_cycles > 0 {
        self.a3_wait_cycles -= 1;
    } else {
        // A3 wait complete, transition to DriveData.
        self.process_latency_elapsed();
    }
}

// AFTER — add timing check:
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
    if self.a3_wait_cycles > 0 {
        self.a3_wait_cycles -= 1;
    } else {
        // Check access time using virtual time
        let virtual_time = self.current_virtual_time();
        if self.state_machine.is_latency_elapsed_at(virtual_time) {
            self.process_latency_elapsed();
        } else {
            // TIMING VIOLATION: Access time not yet elapsed
            // Component produces undefined results (just like real hardware)
            // The ROM will NOT drive data — the bus remains undefined
            // Violation is recorded but simulation continues
        }
    }
}

2.9 — Update execute_state_logic() WaitLatency branch

Similarly update the execute_state_logic() WaitLatency branch:

// BEFORE (line 275-280):
Intel400xUnifiedState::WaitLatency => {
    if self.state_machine.is_latency_elapsed() {
        self.process_latency_elapsed();
    }
}

// AFTER:
Intel400xUnifiedState::WaitLatency => {
    let virtual_time = self.current_virtual_time();
    if self.state_machine.is_latency_elapsed_at(virtual_time) {
        self.process_latency_elapsed();
    }
}

2.10 — Update constructor and imports

// Add to new_with_access_time():
virtual_clock: None,

// Add imports:
use crate::virtual_clock::VirtualClock;
use crate::component_timing::ComponentTiming;

Step 3: Intel 4002 RAM

Current Timing Mechanism

Intel4002 follows the same pattern as ROM but adds:

State machine flow:

Idle → AddressPhase → WaitLatency → ReadData/WriteData/OutputPort → Idle

Timing Enforcement Points

Same as ROM — the critical check is in WaitLatency state when transitioning to data operations.

Required Changes

Identical pattern to ROM (Step 2):

3.1 — Add virtual_clock: Option<Arc<VirtualClock>> field + helper

3.2 — Add set_virtual_clock() method

3.3 — Update Intel400xTimingState impl (Instant → Duration)

3.4 — Implement update_at_time() (same structure as ROM)

3.5 — Update update() to delegate to update_at_time()

3.6 — Update run() with VirtualClock wait pattern

3.7 — Implement timing() returning ComponentTiming::intel_4002()

3.8 — Update handle_phi2_rising() WaitLatency check (line ~479-481):

// BEFORE:
if self.state_machine.is_latency_elapsed() {
    self.process_latency_elapsed();
}

// AFTER:
let virtual_time = self.current_virtual_time();
if self.state_machine.is_latency_elapsed_at(virtual_time) {
    self.process_latency_elapsed();
}

3.9 — Update execute_state_logic() WaitLatency branch (line ~266-269)

3.10 — Update constructor and imports


Step 4: Intel 4004 CPU

Current Timing Mechanism

Intel4004 has its own MemoryState enum and timing fields, separate from the shared Intel400xStateMachine:

Location Line Current Code Purpose
address_latch_time field 168 address_latch_time: Option<Instant> Timestamp when address was latched
access_time field 170 access_time: Duration Memory access time (500ns)
advance_address_cycle() 596 self.address_latch_time = Some(Instant::now()) Record latch time at A2→A3 transition
thread::sleep() 2387 In run() Busy-wait loop

Key difference: The CPU does NOT use the shared Intel400xStateMachine. It has its own MemoryState enum and AddressCycle enum. The address_latch_time field is directly on the CPU struct.

Timing Enforcement Points

The CPU's timing enforcement is different from memory components:

  • Propagation delay (150ns): After the CPU drives address/data onto the bus, its outputs don't change instantly
  • Setup/hold time checks: The CPU should verify that data from ROM/RAM was stable for the required setup time before the clock edge
  • Frequency check: The CPU should verify the clock frequency is within spec (750 kHz max)
graph TD
    A[A1: Drive addr high nibble] --> B[A2: Drive addr low nibble]
    B -->|address_latch_time = virtual_time| C[A3: Wait for ROM/RAM]
    C -->|Wait access_time| D[M1: Read OPR from bus]
    D --> E[M2: Read OPA from bus]
    E --> F[Execute instruction]

Required Changes

4.1 — Change address_latch_time from Instant to Duration

// BEFORE (line 168):
address_latch_time: Option<Instant>,

// AFTER:
address_latch_time: Option<Duration>,  // Virtual time when address was latched

4.2 — Add VirtualClock field and helper

// Add to struct Intel4004:
virtual_clock: Option<Arc<VirtualClock>>,

fn current_virtual_time(&self) -> Duration {
    self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}

4.3 — Add set_virtual_clock() method

pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
    self.virtual_clock = Some(clock);
}

4.4 — Update advance_address_cycle() (line 596)

// BEFORE:
self.address_latch_time = Some(Instant::now());

// AFTER:
self.address_latch_time = Some(self.current_virtual_time());

4.5 — Implement update_at_time()

fn update_at_time(&mut self, virtual_time: Duration) {
    if !self.is_running() {
        return;
    }

    // ... same clock edge detection logic as current update() ...
    // The key difference: address_latch_time is now Duration-based,
    // so any elapsed time checks use virtual_time - address_latch_time
}

4.6 — Update update() to delegate

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

4.7 — Update run() with VirtualClock wait pattern

fn run(&mut self) {
    self.base.set_running(true);

    let vclock = match self.virtual_clock.as_ref() {
        Some(vc) => Arc::clone(vc),
        None => {
            // Legacy busy-wait loop
            while self.is_running() {
                self.update();
                thread::sleep(Duration::from_micros(1));
                if self.cycle_count > 100000 {
                    // ... safety check ...
                }
            }
            return;
        }
    };

    let timing = ComponentTiming::intel_4004();
    while self.is_running() {
        let next_update = vclock.now() + timing.min_update_interval;
        match vclock.wait_until(next_update) {
            Some(current_vtime) => {
                if !self.is_running() { break; }
                self.update_at_time(current_vtime);
                if self.cycle_count > 100000 {
                    // ... safety check ...
                }
            }
            None => break,
        }
    }
}

4.8 — Implement timing() trait method

fn timing(&self) -> ComponentTiming {
    ComponentTiming::intel_4004()
}

4.9 — Update constructor and reset methods

// In new() constructor:
address_latch_time: None,  // Already None, just type change
virtual_clock: None,

// In reset methods (lines 278, 497, 705):
self.address_latch_time = None;  // No change needed — already None

4.10 — Add imports

use crate::virtual_clock::VirtualClock;
use crate::component_timing::ComponentTiming;

Timing Violation Handling Strategy

Timing violations are recorded, not fatal. When a violation is detected:

  1. Access time not elapsed (ROM/RAM): The component stays in WaitLatency and does NOT drive data onto the bus. The bus remains in its previous state (possibly undefined). The CPU reads garbage — just like real hardware.

  2. Overclocked frequency: Detected when the clock period is shorter than min_clock_period(). The component continues operating but may produce incorrect results. Violation is logged.

  3. Setup/hold time violation: The latched value may be incorrect. The component continues with the corrupted value — just like real hardware.

Violation Recording (Future Enhancement)

For Phase 6, violations are detected at the enforcement points but not yet recorded to a global log. The component simply behaves incorrectly (doesn't drive data, latches wrong values, etc.). A TimingViolationLog can be added in a future phase to collect violations for display in the GUI.

The ComponentTiming check methods already return Result<(), TimingViolation>:

These can be used at enforcement points to generate violations for logging when that infrastructure is added.


Summary of Changes by File

intel_400x.rs (Shared)

Change Type
address_latch_time: Option<Instant> → Option<Duration> Field type change
get/set_address_latch_time() return/param type Signature change
Add is_latency_elapsed_at(virtual_time: Duration) New method
Add handle_address_nibble_at(nibble, virtual_time) New method
Add set_address_latch_vtime(time: Duration) New method
Add virtual-time methods to Intel400xTimingState trait Trait extension
Add handle_latency_wait_at() to Intel400xAddressHandling trait Trait extension
Keep existing is_latency_elapsed() for backward compat No change

intel_4001.rs (ROM)

Change Type
Add virtual_clock: Option<Arc<VirtualClock>> field New field
Add set_virtual_clock() method New method
Add current_virtual_time() helper New method
Implement update_at_time(virtual_time: Duration) New method
Update update() to delegate to update_at_time() Modify
Update run() with VirtualClock wait pattern Modify
Implement timing() returning ComponentTiming::intel_4001() New method
Update Intel400xTimingState impl (Instant → Duration) Modify
Update handle_phi2_rising() WaitLatency check Modify
Update execute_state_logic() WaitLatency check Modify
Update constructor (virtual_clock: None) Modify
Add imports for VirtualClock, ComponentTiming Modify

intel_4002.rs (RAM)

Change Type
Same changes as ROM, but with ComponentTiming::intel_4002() Same pattern
Update handle_phi2_rising() WaitLatency check Modify
Update execute_state_logic() WaitLatency check Modify

intel_4003.rs (Shift Register)

Change Type
Same pattern as ROM, but with ComponentTiming::intel_4003() Same pattern
No handle_phi2_rising() WaitLatency changes needed (4003 doesn't wait for access time the same way) N/A

intel_4004.rs (CPU)

Change Type
address_latch_time: Option<Instant> → Option<Duration> Field type change
Add virtual_clock: Option<Arc<VirtualClock>> field New field
Add set_virtual_clock() method New method
Add current_virtual_time() helper New method
Implement update_at_time(virtual_time: Duration) New method
Update update() to delegate to update_at_time() Modify
Update run() with VirtualClock wait pattern Modify
Implement timing() returning ComponentTiming::intel_4004() New method
Update advance_address_cycle() latch time assignment Modify
Update constructor (virtual_clock: None) Modify
Add imports for VirtualClock, ComponentTiming Modify

Backward Compatibility

All changes follow the Phase 5 pattern:

  • Option<Arc<VirtualClock>> — components work without a VirtualClock
  • run() falls back to legacy thread::sleep() busy-wait when no VirtualClock is injected
  • update() delegates to update_at_time() using current_virtual_time() (returns Duration::ZERO when no clock)
  • update_at_time() is a new override; existing update() callers continue to work
  • Existing is_latency_elapsed() method kept for backward compat (uses Instant::elapsed() internally)

Phase 7 (ConfigurableSystem) will create the Arc<VirtualClock> and inject it via set_virtual_clock() on each component.


Testing Strategy

After each component migration, verify:

  1. Legacy path still works — Run existing tests without VirtualClock injected
  2. Virtual time path works — Inject a VirtualClock, advance time, verify correct behavior
  3. Timing violation behavior — Overclock the clock and verify ROM/RAM produce incorrect results while the faster shift register still works
  4. State machine transitions — Verify Idle → AddressPhase → WaitLatency → DriveData cycle completes correctly in virtual time