rigless 0.2.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! A run as the core sees it: the session and the driver behind the calls the core makes between
//! instructions, with their types out of the way.

use std::any::Any;
use std::collections::BTreeMap;
use std::sync::OnceLock;

use crate::emulator::{Access, Driver, Hit, Host, Settled, Stop, Watch};
use crate::machine::Board;
use crate::session::Session;
use crate::snapshot;
use crate::soc::Width;
use crate::time::VirtualNanos;

/// A run as the core sees it, without its types.
pub(super) trait Driven: Send {
    fn first_deadline(&mut self) -> VirtualNanos;
    /// The value read or written back, the next deadline, and whether the access failed.
    fn access(&mut self, access: Access, host: &mut dyn Host) -> (u64, VirtualNanos, bool);
    /// Whether the last access a debugger made reached nothing that answers it.
    fn refused(&mut self) -> bool;
    /// The processor can't stop at the instruction `watch` names, which fails the run.
    fn unwatched(&mut self, watch: Watch);
    fn stop(&mut self, stop: Stop, host: &mut dyn Host) -> Option<VirtualNanos>;
    fn hit(&mut self, hit: Hit, host: &mut dyn Host) -> VirtualNanos;
    fn into_any(self: Box<Self>) -> Box<dyn Any + Send>;
    /// What a debugger's read of `width` at `address` answers at `at`, with nothing in the run
    /// changed, or `None` where nothing answers it.
    fn peek(&mut self, _at: VirtualNanos, _address: u64, _width: Width) -> Option<u64> {
        None
    }

    /// The firmware wrote `bytes` to the host's console at `at`, after `instruction_count`
    /// instructions. Answers whether that's news, which a run going back over what it did isn't.
    fn console(&mut self, _at: VirtualNanos, _instruction_count: u64, _bytes: &[u8]) -> bool {
        true
    }

    /// A debugger's breakpoint printed `text` at `at`, after `instruction_count` instructions.
    fn debugger_print(&mut self, _at: VirtualNanos, _instruction_count: u64, _text: &[u8]) {}

    /// A debugger took the core at `at`, after `instruction_count` instructions, or let it go.
    fn debugger_attached(&mut self, _at: VirtualNanos, _instruction_count: u64, _attached: bool) {}

    /// Whether the driver lets a debugger take the run back.
    fn may_rewind(&self) -> bool {
        false
    }

    /// What a checkpoint keeps of the run, or why it can't.
    fn keep(&self) -> Result<Kept, snapshot::Error> {
        Err(snapshot::Error::Failed)
    }

    /// Puts back what [`Driven::keep`] kept, with the run's own watches settled again on `host`.
    fn put_back(&mut self, _kept: &Kept, _host: &mut dyn Host) -> Result<(), snapshot::Error> {
        Err(snapshot::Error::Failed)
    }

    /// Whether the run is going back over calls the driver has had.
    fn retracing(&self) -> bool {
        false
    }

    /// The run goes a new way from here, at `at`, so the driver and the board's hosts hear
    /// everything from now on.
    fn new_way(&mut self, _at: VirtualNanos) {}
}

/// What a checkpoint keeps of a run: the session's state, the stop the driver ended the run at, and
/// how many times the driver had been called.
#[derive(Debug)]
pub(super) struct Kept {
    session: Vec<u8>,
    last: Option<Stop>,
    called: u64,
    /// The session's state as a model reads it back, kept for the process the first time it's put
    /// back, since a model's name points into it.
    // ceiling: a checkpoint's session state, once put back, stays until the process ends; upgrade
    // when a session reads back without lending its names
    read: OnceLock<&'static [u8]>,
}

#[cfg(fuzzing)]
impl Kept {
    /// What a run with no session keeps, which is nothing.
    pub(super) fn nothing() -> Self {
        Self {
            session: Vec::new(),
            last: None,
            called: 0,
            read: OnceLock::new(),
        }
    }
}

pub(super) struct Run<D: Board + ?Sized, R: Driver<D>> {
    pub(super) session: Session<D>,
    pub(super) driver: R,
    pub(super) last: Option<Stop>,
    /// The stop a snapshot was taken at, until the run started from it gets back there.
    pub(super) resuming: Option<Stop>,
    /// How many times the run has called the driver, along the run as it stands.
    pub(super) called: u64,
    /// The most calls the driver has had, which a run going back over what it did makes once.
    pub(super) furthest: u64,
    /// What `on_stop` answered, by the number of its call, which a run going back over it answers
    /// again. Only a driver that lets the run go back has its answers kept.
    pub(super) answered: BTreeMap<u64, VirtualNanos>,
}

impl<D: Board + ?Sized, R: Driver<D>> Run<D, R> {
    /// `session` driven by `driver`, from a snapshot taken at `resuming` when there's one.
    pub(super) fn new(session: Session<D>, driver: R, resuming: Option<Stop>) -> Self {
        Self {
            session,
            driver,
            last: None,
            resuming,
            called: 0,
            furthest: 0,
            answered: BTreeMap::new(),
        }
    }

    /// The next deadline, taken after the driver, which may have asked for an earlier one. A resumed
    /// run keeps the stop it was taken at armed until it gets back there, or the core would run past
    /// the instant the resume belongs at.
    fn next_deadline(&mut self) -> VirtualNanos {
        match self.resuming {
            Some(taken) => taken.at,
            None => self.session.next_deadline(),
        }
    }

    /// Counts a call the run makes to the driver, and answers whether to make it: whether the
    /// driver hasn't had it before.
    fn heard(&mut self) -> bool {
        let fresh = self.called >= self.furthest;
        self.called += 1;
        self.furthest = self.furthest.max(self.called);
        fresh
    }
}

impl<D: Board + ?Sized + 'static, R: Driver<D> + 'static> Driven for Run<D, R> {
    fn first_deadline(&mut self) -> VirtualNanos {
        self.next_deadline()
    }

    fn access(&mut self, access: Access, host: &mut dyn Host) -> (u64, VirtualNanos, bool) {
        let (value, _) = self.session.access(access, host);
        let failed = self.session.take_failure();
        if self.heard() {
            self.driver
                .on_settled(&mut self.session, host, Settled::Access { access, value });
        }
        self.session.settle_watches(host);
        (value, self.next_deadline(), failed)
    }

    fn refused(&mut self) -> bool {
        self.session.take_refused()
    }

    fn unwatched(&mut self, watch: Watch) {
        self.session.refuse_watch(watch);
    }

    fn hit(&mut self, hit: Hit, host: &mut dyn Host) -> VirtualNanos {
        if self.heard() {
            self.driver
                .on_settled(&mut self.session, host, Settled::Hit(hit));
        }
        self.session.settle_watches(host);
        self.next_deadline()
    }

    fn stop(&mut self, stop: Stop, host: &mut dyn Host) -> Option<VirtualNanos> {
        if let Some(taken) = self.resuming.take() {
            // The snapshot was taken inside this same stop, once the models had stepped to it and
            // the driver had answered it, so what's left of it is the resume.
            assert!(
                stop.at == taken.at && stop.instruction_count == taken.instruction_count,
                "resumed at {} ns after {} instructions, and the snapshot was taken at {} ns after {}",
                stop.at.0,
                stop.instruction_count,
                taken.at.0,
                taken.instruction_count
            );
            self.driver
                .on_settled(&mut self.session, host, Settled::Stop(stop));
            self.session.settle_watches(host);
            let until = self.session.until();
            return Some(
                self.session
                    .resume(until, taken.at, taken.instruction_count, host),
            );
        }
        self.session.stop(stop, host);
        if self.heard() {
            self.driver
                .on_settled(&mut self.session, host, Settled::Stop(stop));
        }
        self.session.settle_watches(host);
        if !self.session.reached(&stop) {
            return Some(self.session.next_deadline());
        }
        // The stop ends the run only when the driver doesn't carry on past it, so a run that the
        // firmware ends later wasn't ended by it.
        let call = self.called;
        let until = if self.heard() {
            let until = self.driver.on_stop(&mut self.session, stop, host);
            if let Some(until) = until
                && self.driver.may_rewind()
            {
                self.answered.insert(call, until);
            }
            until
        } else {
            self.answered.get(&call).copied()
        };
        self.last = until.is_none().then_some(stop);
        let until = until?;
        self.session.settle_watches(host);
        Some(
            self.session
                .resume(until, stop.at, stop.instruction_count, host),
        )
    }

    fn into_any(self: Box<Self>) -> Box<dyn Any + Send> {
        self
    }

    fn peek(&mut self, at: VirtualNanos, address: u64, width: Width) -> Option<u64> {
        self.session.peek(at, address, width)
    }

    fn console(&mut self, at: VirtualNanos, instruction_count: u64, bytes: &[u8]) -> bool {
        let heard = self.heard();
        if heard {
            self.driver.on_console(at, instruction_count, bytes);
        }
        heard
    }

    fn debugger_print(&mut self, at: VirtualNanos, instruction_count: u64, text: &[u8]) {
        self.driver.on_debugger_print(at, instruction_count, text);
    }

    fn debugger_attached(&mut self, at: VirtualNanos, instruction_count: u64, attached: bool) {
        self.driver
            .on_debugger_attached(at, instruction_count, attached);
    }

    fn may_rewind(&self) -> bool {
        self.driver.may_rewind()
    }

    fn keep(&self) -> Result<Kept, snapshot::Error> {
        if self.resuming.is_some() {
            return Err(snapshot::Error::Resuming);
        }
        Ok(Kept {
            session: snapshot::stored(&self.session)?,
            last: self.last,
            called: self.called,
            read: OnceLock::new(),
        })
    }

    fn put_back(&mut self, kept: &Kept, host: &mut dyn Host) -> Result<(), snapshot::Error> {
        let read = kept
            .read
            .get_or_init(|| snapshot::keep(kept.session.clone()));
        snapshot::load(&mut self.session, read)?;
        self.session.settle_watches(host);
        self.last = kept.last;
        self.called = kept.called;
        Ok(())
    }

    fn retracing(&self) -> bool {
        self.called < self.furthest
    }

    fn new_way(&mut self, at: VirtualNanos) {
        self.furthest = self.called;
        self.answered.split_off(&self.called);
        self.session.new_way(at);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(feature = "pixracer")]
    use crate::chip::Timeline;
    #[cfg(feature = "pixracer")]
    use crate::chip::uart;
    use crate::device::Level;
    use crate::emulator::{
        AccessKind, Error, HitKind, InterruptLine, Memory as GuestMemory, StopReason,
    };
    use crate::machine::all;
    #[cfg(feature = "pixracer")]
    use crate::machine::pixracer::Pixracer;

    /// Nothing but a host with no memory and no lines, and a driver that ends the run.
    struct Nothing;

    impl GuestMemory for Nothing {
        fn read(&mut self, address: u64, width: Width) -> Result<u64, Error> {
            Err(Error::Memory { address, width })
        }

        fn write(&mut self, address: u64, width: Width, _value: u64) -> Result<(), Error> {
            Err(Error::Memory { address, width })
        }
    }

    impl Host for Nothing {
        fn set_interrupt(&mut self, _line: InterruptLine, _level: Level) {}

        fn request_reset(&mut self) {}
    }

    impl<D: Board + ?Sized> Driver<D> for Nothing {
        fn on_stop(
            &mut self,
            _session: &mut Session<D>,
            _stop: Stop,
            _host: &mut dyn Host,
        ) -> Option<VirtualNanos> {
            None
        }
    }

    /// An access or a hit before a resumed run gets back to its stop answers the stop's own instant,
    /// so the core keeps it armed.
    #[test]
    fn an_access_or_a_hit_before_a_resumed_run_is_back_at_its_stop_keeps_the_stop_armed() {
        let Some(&(_, build)) = all().first() else {
            return;
        };
        let taken = Stop {
            at: VirtualNanos(5_000_000),
            instruction_count: 1_250_000,
            reason: StopReason::Deadline,
        };
        let session = Session::new(build().unwrap(), 0, VirtualNanos(1)).unwrap();
        let mut run = Run::new(session, Nothing, Some(taken));
        assert_eq!(run.first_deadline(), taken.at);
        let read = Access {
            kind: AccessKind::Read,
            address: 0,
            width: Width::W32,
            value: 0,
            instruction_count: taken.instruction_count,
            virtual_time: taken.at,
            program_counter: None,
        };
        assert_eq!(run.access(read, &mut Nothing).1, taken.at);
        let hit = Hit {
            kind: HitKind::Instruction,
            address: 0,
            at: taken.at,
            instruction_count: taken.instruction_count,
        };
        assert_eq!(run.hit(hit, &mut Nothing), taken.at);
    }

    /// Pauses the run where each access stood, a nanosecond on, and counts the stops it's handed.
    struct Pausing(u32);

    impl<D: Board + ?Sized> Driver<D> for Pausing {
        fn on_settled(&mut self, session: &mut Session<D>, _host: &mut dyn Host, settled: Settled) {
            match settled {
                Settled::Access { access, .. } => {
                    session.pause_at(Some(VirtualNanos(access.virtual_time.0 + 1)));
                }
                Settled::Stop(_) => self.0 += 1,
                Settled::Hit(_) => {}
            }
        }

        fn on_stop(
            &mut self,
            _session: &mut Session<D>,
            _stop: Stop,
            _host: &mut dyn Host,
        ) -> Option<VirtualNanos> {
            None
        }
    }

    /// An access answers the deadline as the driver left it, so a pause it asks for right after one
    /// is where the core stops next. A stop there is the driver's alone: it's handed the stop and
    /// the run isn't over.
    #[test]
    fn an_access_answers_the_pause_the_driver_asked_for_there_and_the_pause_is_the_drivers_alone() {
        let Some(&(_, build)) = all().first() else {
            return;
        };
        let session = Session::new(build().unwrap(), 0, VirtualNanos(1_000_000)).unwrap();
        let mut run = Run::new(session, Pausing(0), None);
        let read = Access {
            kind: AccessKind::Read,
            address: 0,
            width: Width::W32,
            value: 0,
            instruction_count: 100,
            virtual_time: VirtualNanos(400),
            program_counter: Some(0x1000),
        };
        assert_eq!(run.access(read, &mut Nothing).1, VirtualNanos(401));
        let paused = Stop {
            at: VirtualNanos(401),
            instruction_count: 101,
            reason: StopReason::Deadline,
        };
        assert!(run.stop(paused, &mut Nothing).is_some(), "the run goes on");
        assert_eq!(run.driver.0, 1);
        assert_eq!(run.last, None, "and it wasn't the instant asked for");
    }

    /// Carries the run on a millisecond past each stop it's handed.
    struct CarriesOn;

    impl<D: Board + ?Sized> Driver<D> for CarriesOn {
        fn on_stop(
            &mut self,
            _session: &mut Session<D>,
            stop: Stop,
            _host: &mut dyn Host,
        ) -> Option<VirtualNanos> {
            Some(VirtualNanos(stop.at.0 + 1_000_000))
        }
    }

    /// A stop the driver carries the run on past isn't the stop that ended it, so a run the
    /// firmware ends after it was ended by no stop, and one the driver ends at a stop was.
    #[test]
    fn only_the_stop_the_driver_ends_the_run_at_is_kept() {
        let Some(&(_, build)) = all().first() else {
            return;
        };
        let at = VirtualNanos(1_000_000);
        let reached = Stop {
            at,
            instruction_count: 250_000,
            reason: StopReason::Deadline,
        };
        let session = Session::new(build().unwrap(), 0, at).unwrap();
        let mut run = Run::new(session, CarriesOn, None);
        assert!(run.stop(reached, &mut Nothing).is_some(), "the run goes on");
        assert_eq!(run.last, None);

        let session = Session::new(build().unwrap(), 0, at).unwrap();
        let mut run = Run::new(session, Nothing, None);
        assert!(run.stop(reached, &mut Nothing).is_none(), "the run ends");
        assert_eq!(run.last, Some(reached));
    }

    /// What a driver that never lets the run go back answered at each stop is kept nowhere, however
    /// many stops it carries the run on past.
    #[test]
    fn a_driver_that_never_goes_back_has_no_answer_kept() {
        let Some(&(_, build)) = all().first() else {
            return;
        };
        let mut until = VirtualNanos(1_000_000);
        let session = Session::new(build().unwrap(), 0, until).unwrap();
        let mut run = Run::new(session, CarriesOn, None);
        for _ in 0..3 {
            let reached = Stop {
                at: until,
                instruction_count: until.0 / 4,
                reason: StopReason::Deadline,
            };
            until = run.stop(reached, &mut Nothing).expect("the run goes on");
        }
        assert!(run.answered.is_empty());
    }

    /// A far end that keeps where the run went a new way from.
    #[cfg(feature = "pixracer")]
    #[derive(Debug, Default)]
    struct NewWay(Option<VirtualNanos>);

    #[cfg(feature = "pixracer")]
    impl snapshot::Store for NewWay {
        fn store(&self, _to: &mut snapshot::Writer) -> Result<(), snapshot::Error> {
            Ok(())
        }

        fn load(&mut self, _from: &mut snapshot::Reader) -> Result<(), snapshot::Error> {
            Ok(())
        }
    }

    #[cfg(feature = "pixracer")]
    impl uart::Peer for NewWay {
        fn receive(&mut self, _word: u16, _context: &mut Timeline<'_, '_>) {}

        fn transmit(&mut self, _now: VirtualNanos) -> Option<u16> {
            None
        }

        fn forget_after(&mut self, now: VirtualNanos) {
            self.0 = Some(now);
        }
    }

    /// A run that goes a new way tells each host on the board's wires where from.
    #[test]
    #[cfg(feature = "pixracer")]
    fn a_new_way_reaches_the_boards_hosts() {
        let mut board = Pixracer::new().unwrap();
        let wire = board.hosts()[0].name;
        assert!(board.attach_host(wire, Box::new(NewWay::default())));
        let board: Box<dyn Board> = Box::new(board);
        let session = Session::new(board, 0, VirtualNanos(1)).unwrap();
        let mut run = Run::new(session, Nothing, None);
        run.new_way(VirtualNanos(500));
        let host: &mut dyn Any = run.session.board_mut().host_mut(wire).unwrap();
        let heard = host.downcast_mut::<NewWay>().unwrap();
        assert_eq!(heard.0, Some(VirtualNanos(500)));
    }
}