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.
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:
intel_400x.rs— Shared state machine timing (highest priority)intel_4001.rs— Intel 4001 ROMintel_4002.rs— Intel 4002 RAMintel_4003.rs— Intel 4003 Shift Registerintel_4004.rs— Intel 4004 CPU
Files NOT modified (Phase 7 responsibility):
system_config.rs— Will injectArc<VirtualClock>into componentsvirtual_clock.rs— Already completecomponent_timing.rs— Already complete
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:
intel_400x.rsfirst — The shared state machine containsaddress_latch_time: Option<Instant>andis_latency_elapsed()used by all three memory components. Changing this first unblocks all component migrations.- Intel 4003 second — Simplest component; no address latching in the traditional sense, uses shift/load operations. Good validation of the shared code changes.
- Intel 4001 third — ROM is simpler than RAM (read-only, no write operations). Validates the full address→wait→drive flow.
- Intel 4002 fourth — RAM adds write operations and I/O port states on top of the ROM pattern.
- Intel 4004 last — CPU is the most complex (98K chars, own MemoryState enum, own
address_latch_timefield). 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
Intel400xStateMachinefor state management - Implements
Intel400xTimingStatetrait - Has no direct
Instant::now()calls — all timing goes through the shared state machine - Has
thread::sleep(Duration::from_micros(1))inrun()(line 597) - Uses
prev_phi1/prev_phi2for 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:
- Read/Write/OutputPort states (vs. just DriveData)
is_latency_elapsed()check inexecute_state_logic()WaitLatencyis_latency_elapsed()check inhandle_phi2_rising()thread::sleep()inrun()(line 726)
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:
-
Access time not elapsed (ROM/RAM): The component stays in
WaitLatencyand does NOT drive data onto the bus. The bus remains in its previous state (possibly undefined). The CPU reads garbage — just like real hardware. -
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. -
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>:
check_access_time(elapsed)— ReturnsErr(TimingViolation::AccessTimeNotElapsed)if not enough timecheck_setup_time(stability_duration)— ReturnsErr(TimingViolation::SetupTimeViolation)check_hold_time(stability_duration)— ReturnsErr(TimingViolation::HoldTimeViolation)supports_frequency(freq_hz)— Returnsbool
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 VirtualClockrun()falls back to legacythread::sleep()busy-wait when no VirtualClock is injectedupdate()delegates toupdate_at_time()usingcurrent_virtual_time()(returnsDuration::ZEROwhen no clock)update_at_time()is a new override; existingupdate()callers continue to work- Existing
is_latency_elapsed()method kept for backward compat (usesInstant::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:
- Legacy path still works — Run existing tests without VirtualClock injected
- Virtual time path works — Inject a VirtualClock, advance time, verify correct behavior
- Timing violation behavior — Overclock the clock and verify ROM/RAM produce incorrect results while the faster shift register still works
- State machine transitions — Verify Idle → AddressPhase → WaitLatency → DriveData cycle completes correctly in virtual time