rigless 0.0.6

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 fork of QEMU built in to execute the instructions
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
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! One firmware on one target of one machine: the bus, the events, and what the run records.
//!
//! `Session`, in `rigless::session`, is the models' side of a run, called by whatever executes the
//! instructions. The emulator calls [`Session::access`] for every peripheral access as the core
//! makes it, at the access's own instant: a read answers with the model's or the register file's
//! value and a write applies to it, the models that asked for an access of their own or drove a
//! request get it at that same instant, and every line a model set is driven before the next
//! instruction executes. The answer carries the next instant the models need control at, the
//! earlier of the next event and where the run is asked to reach, and the emulator stops there and
//! calls [`Session::stop`]: the events due fire in order, every model gets a look outside, the part
//! settles again, and the answer is the next instant once more. A run is asked to reach an instant
//! with [`Session::run_until`] before it starts, and with [`Session::resume`] at each stop after. A
//! stop at or past it is [`Session::reached`], and whoever drives the run decides what comes next.
//!
//! An access the bus refuses because no register covers the address, or the access runs past the
//! register that does, answers zero and is remembered once with the program counter of its first
//! occurrence, which [`Session::unmapped`] lists by ascending address. One it refuses although it
//! carries the register, because a gate the part declares is shut, is remembered with its fault and
//! listed by [`Session::refused`], as is every access a model asked for that the bus wouldn't make.
//! An access to a register no model implements, served from the register file, is remembered for
//! [`Session::unimplemented`], and an interrupt the machine's route has no line for as instance and
//! number for [`Session::unrouted`]. A reset the firmware requests is a stop of its own: at its
//! instant the part restarts as [`crate::mcu::Mcu::reset`] leaves it, the emulator lowers every line
//! the part drove, the instant is remembered for [`Session::resets`], and the run goes on from the
//! restarted core, which finds flash and memory as the firmware left them. A model asking through
//! [`crate::device::DeviceContext::request_reset`] restarts the part the same way, inside the access
//! or the stop that carried the ask, with no instruction of the core's own in between. A core halted
//! when an instant comes has had its clock warped to it, which is the instant reached as well. A
//! model that finds its own instance in a state the manual forbids, checked against another instance
//! after the access already landed, has nothing to refuse and tells the run instead, listed by
//! [`Session::told`].

use std::collections::{BTreeMap, BTreeSet};
use std::fmt;

use crate::device::{InterruptChange, ResetCause, Told};
use crate::emulator::{Access, AccessKind, Error, Host, InterruptLine, Stop, StopReason};
use crate::machine::Board;
use crate::mcu::{Mcu, Refused};
use crate::soc::{Bus, Fault};
use crate::time::VirtualNanos;

/// A firmware on one target of a machine, from the first access to the last stop.
///
/// The session owns the board and steps one of its MCUs. The board keeps every model reachable by
/// field once the run has ended, through [`Session::board`].
#[derive(Debug)]
pub struct Session<D: Board + ?Sized = dyn Board> {
    board: Box<D>,
    target: usize,
    route: fn(u16) -> Option<InterruptLine>,
    // Whether each slot of the target's bus has a model, which is fixed once the MCU is built.
    backed: Box<[bool]>,
    until: VirtualNanos,
    report: Report,
    // A settle inside an access cannot answer with its error, so it is kept for the driver.
    failed: Option<Error>,
}

// What the bus refused or answered from a register file alone, kept for the run's report.
#[derive(Debug, Default)]
struct Report {
    unmapped: BTreeMap<u64, u64>,
    unrouted: BTreeSet<(&'static str, u16)>,
    refused: BTreeMap<u64, Fault>,
    unimplemented: BTreeMap<u64, Unimplemented>,
    resets: Vec<Reset>,
    told: BTreeMap<(&'static str, &'static str, &'static str), Told>,
}

/// Keeps the first telling of one condition: same instance, register and reason, by whichever came
/// first, the way [`answered_from_a_file_alone`] keeps the first program counter for one address.
fn note_told(report: &mut Report, told: impl Iterator<Item = Told>) {
    for told in told {
        report
            .told
            .entry((told.instance, told.register, told.reason))
            .or_insert(told);
    }
}

/// One restart the target took, in the order it took it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Reset {
    /// The virtual clock at the restart.
    pub at: VirtualNanos,
    /// The emulator's instruction count at the restart.
    pub instructions: u64,
    /// The core's own request, or the instance that asked for one.
    pub cause: ResetCause,
}

/// An access to a register the part carries and no model implements, which the bus served from the
/// register file.
///
/// The instance is one the part carries and has no model for, so a read answers the register's
/// value and a write lands in it and nothing reacts. That is not a fault and the firmware is not
/// told: it is a gap between what the part describes and what it models, and a firmware that waits
/// on something only a model would do waits forever. A write is the sharper half: a firmware writes
/// to hardware to make something happen, and here nothing did.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unimplemented {
    /// The instance, as the part names it.
    pub instance: &'static str,
    /// The register covering the address, as the part names it.
    pub register: &'static str,
    /// The program counter of the first access here.
    pub program_counter: u64,
    /// How many reads were answered from the register file.
    pub reads: u64,
    /// How many writes landed in it.
    pub writes: u64,
    /// What the last of those writes carried, so the report reads on its own without the bus.
    pub written: Option<u64>,
}

/// An address the bus refused because no register covers it, or the access runs past the register
/// that does, and where the firmware was when it first asked. It prints in hex, which is how a person
/// reads an address: `unmapped at=0x40000100 pc=0x08000098` as a line, and the same two numbers as a
/// struct for `{:?}`.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Unmapped {
    /// The address, which no model claims.
    pub address: u64,
    /// The program counter of the first access to it.
    pub program_counter: u64,
}

impl fmt::Display for Unmapped {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "unmapped at={:#010x} pc={:#010x}",
            self.address, self.program_counter
        )
    }
}

impl fmt::Debug for Unmapped {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Unmapped")
            .field("address", &format_args!("{:#010x}", self.address))
            .field(
                "program_counter",
                &format_args!("{:#010x}", self.program_counter),
            )
            .finish()
    }
}

impl<D: Board + ?Sized> Session<D> {
    /// A session on `board`'s target at `target`, asked to reach `until`.
    ///
    /// The board's targets are its MCUs in its own order, and a board with one has it at zero. A target
    /// whose command aliases a region its MCU's memory map does not have is refused here, before the
    /// emulator is started.
    pub fn new(mut board: Box<D>, target: usize, until: VirtualNanos) -> Result<Self, Error> {
        let machine = board.name();
        let count = board.target_names().len();
        let (name, route, aliases) = {
            let targets = board.targets();
            let chosen = targets.get(target).ok_or(Error::NoTarget {
                machine,
                target,
                count,
            })?;
            (
                chosen.name,
                chosen.interrupt_line,
                chosen.emulator.aliases.clone(),
            )
        };
        let backed = {
            let mcu = board.mcu(target).ok_or(Error::NoTarget {
                machine,
                target,
                count,
            })?;
            // The emulator would find nothing to put at the second address and start without it. Refuse it here.
            let regions = mcu.memory();
            if let Some(alias) = aliases
                .iter()
                .find(|alias| regions.iter().all(|region| region.name != alias.region))
            {
                return Err(Error::NoRegion {
                    machine,
                    target: name,
                    region: alias.region,
                    at: alias.at,
                    regions: regions.iter().map(|region| region.name).collect(),
                });
            }
            (0..mcu.bus().slots().len())
                .map(|slot| mcu.split(slot).1.is_some())
                .collect()
        };
        Ok(Self {
            board,
            target,
            route,
            backed,
            until,
            report: Report::default(),
            failed: None,
        })
    }

    /// The index of the target this session steps.
    #[must_use]
    pub fn target(&self) -> usize {
        self.target
    }

    /// The instant the run is asked to reach.
    #[must_use]
    pub fn until(&self) -> VirtualNanos {
        self.until
    }

    /// Asks the run to reach `until`: the next answer names the earlier of the next event and it.
    pub fn run_until(&mut self, until: VirtualNanos) {
        self.until = until;
    }

    /// Whether `stop` is the instant the run was asked to reach: at or past it, and not a reset.
    #[must_use]
    pub fn reached(&self, stop: &Stop) -> bool {
        stop.reason != StopReason::Reset && stop.at >= self.until
    }

    /// The next instant the models need control at: the earlier of the next event and where the run
    /// is asked to reach.
    pub fn next_deadline(&mut self) -> VirtualNanos {
        let until = self.until;
        deadline_before(self.mcu().next_event(), until)
    }

    /// One access at its instant: the value read, or the value written back, and the next instant the
    /// models need control at.
    ///
    /// The models may drive a request or ask for an access of their own. Both happen at the instant
    /// of the access that caused them, through `host`, before the core has its answer, and so does
    /// every line they set.
    pub fn access(&mut self, access: Access, host: &mut dyn Host) -> (u64, VirtualNanos) {
        let Self {
            board,
            target,
            route,
            backed,
            until,
            report,
            failed,
        } = self;
        let route = *route;
        let until = *until;
        let mcu = board.mcu(*target).expect("checked at construction");
        let served = match access.kind {
            AccessKind::Read => mcu.read(access.virtual_time, access.address, access.width),
            AccessKind::Write => mcu
                .write(
                    access.virtual_time,
                    access.address,
                    access.width,
                    access.value,
                )
                .map(|()| access.value),
        };
        let value = match served {
            Ok(value) => {
                answered_from_a_file_alone(report, backed, mcu.bus(), &access);
                value
            }
            Err(fault) => {
                refuse(report, access.address, Some(access.program_counter), fault);
                0
            }
        };
        match mcu.settle(host) {
            Ok(refusals) => note(report, refusals),
            Err(error) => {
                failed.get_or_insert(error);
            }
        }
        note_told(report, mcu.drain_told().into_iter());
        // device.rs's request_reset promises no core instruction between the ask and the reset.
        let restarted = take_model_reset(
            report,
            mcu,
            access.virtual_time,
            access.instruction_count,
            host,
        );
        if !restarted {
            apply(
                route,
                mcu.drain_interrupts().into_iter(),
                host,
                &mut report.unrouted,
            );
        }
        (value, deadline_before(mcu.next_event(), until))
    }

    /// The emulator stopped: the events due fire in order, every model gets a look outside, the part
    /// settles, and a reset restarts it. Answers the next instant the models need control at.
    pub fn stop(&mut self, stop: Stop, host: &mut dyn Host) -> VirtualNanos {
        let Self {
            board,
            target,
            route,
            until,
            report,
            failed,
            ..
        } = self;
        let route = *route;
        let until = *until;
        let mcu = board.mcu(*target).expect("checked at construction");
        mcu.advance_to(stop.at);
        let resets_before = report.resets.len();
        if let Some(error) = report.look_outside(mcu, stop.at, stop.instruction_count, route, host)
        {
            failed.get_or_insert(error);
        }
        if stop.reason == StopReason::Reset {
            // The core's own request. A model's reset taken at this same stop is the same restart,
            // with nothing run between them: the part's flags gain the core's cause as well, and the
            // recorded restart keeps the model's. A restart from an earlier stop is not this one, even
            // at the same instant, as with a debugger's reset of a halted core.
            mcu.reset(ResetCause::Core);
            if report.resets.len() == resets_before {
                report.resets.push(Reset {
                    at: stop.at,
                    instructions: stop.instruction_count,
                    cause: ResetCause::Core,
                });
            }
        }
        deadline_before(mcu.next_event(), until)
    }

    /// Asks the run to reach `until` from the stop just taken at `at`, and answers the next deadline.
    ///
    /// Every model looks outside again at `at` first, and what that moved is settled and what it raised
    /// routed. So whatever a driver handed the board at the stop, a byte on a port or an input sensed,
    /// reaches the models at that same stop rather than at the next one.
    pub fn resume(
        &mut self,
        until: VirtualNanos,
        at: VirtualNanos,
        instructions: u64,
        host: &mut dyn Host,
    ) -> VirtualNanos {
        self.until = until;
        let Self {
            board,
            target,
            route,
            report,
            failed,
            ..
        } = self;
        let mcu = board.mcu(*target).expect("checked at construction");
        if let Some(error) = report.look_outside(mcu, at, instructions, *route, host) {
            failed.get_or_insert(error);
        }
        deadline_before(mcu.next_event(), until)
    }

    /// The first error a settle met inside an access or a stop, which the answer could not carry.
    pub fn take_error(&mut self) -> Option<Error> {
        self.failed.take()
    }

    /// The addresses the bus refused because no register covers the address, or the access runs past
    /// the register that does, each with the program counter of its first access, by ascending
    /// address. An entry prints the way `rigless run` prints one.
    #[must_use]
    pub fn unmapped(&self) -> Vec<Unmapped> {
        self.report
            .unmapped
            .iter()
            .map(|(&address, &program_counter)| Unmapped {
                address,
                program_counter,
            })
            .collect()
    }

    /// The accesses the bus refused although it carries the register, and every access a model asked
    /// for that the bus wouldn't make, by ascending address, each with the fault of its first
    /// occurrence.
    ///
    /// This is the gated peripheral: the part declares what has to be true elsewhere for an instance to
    /// answer, and a firmware or a model that reaches one before it is true is refused. It is separate from
    /// [`Session::unmapped`] because the address is modelled and the part said no.
    #[must_use]
    pub fn refused(&self) -> &BTreeMap<u64, Fault> {
        &self.report.refused
    }

    /// The accesses the bus answered from a register file with no model behind it, by ascending address,
    /// each with the instance, the register, and the program counter of its first occurrence.
    ///
    /// An entry whose `writes` is not zero is the one to read first. A read of a reset value is
    /// often exactly right. A write is a firmware asking the part to do something, and here nothing
    /// did.
    ///
    /// Only the core's own accesses are counted. An access a model asks the runtime to make on its behalf
    /// reaches the bus through the settle rather than through an instruction, and the settle answers with
    /// what it refused and not with what it served, so a controller moving a word into an instance with no
    /// model is not reported here.
    #[must_use]
    pub fn unimplemented(&self) -> &BTreeMap<u64, Unimplemented> {
        &self.report.unimplemented
    }

    /// The interrupts the target had no line for, as instance and number, by instance then number.
    #[must_use]
    pub fn unrouted(&self) -> &BTreeSet<(&'static str, u16)> {
        &self.report.unrouted
    }

    /// Every restart the target took, in order, with its instant, its instruction count and its cause.
    /// At each, the target restarted from its reset vector with every peripheral at its reset value,
    /// the board's chips as they stood, and its memory as the firmware left it.
    #[must_use]
    pub fn resets(&self) -> &[Reset] {
        &self.report.resets
    }

    /// What a model told about its own instance without an access to refuse, one entry per distinct
    /// instance, register and reason, each kept from the instant it was first told.
    pub fn told(&self) -> impl Iterator<Item = &Told> {
        self.report.told.values()
    }

    /// The board this session runs on.
    #[must_use]
    pub fn board(&self) -> &D {
        &self.board
    }

    /// The board, whole, to reach a model by field and ask what its boundary recorded.
    ///
    /// The session carries the board's own type, so where the caller built a concrete board this is a
    /// `&mut` to that type, and every model on it is a field path from here. Nothing is downcast.
    pub fn board_mut(&mut self) -> &mut D {
        &mut self.board
    }

    /// The target's MCU.
    pub fn mcu(&mut self) -> &mut dyn Mcu {
        self.board
            .mcu(self.target)
            .expect("checked at construction")
    }
}

// An access the bus served from an instance with no model behind it. Remembered per address, with the
// program counter of the first one, so a polling loop is one line rather than thousands.
fn answered_from_a_file_alone(report: &mut Report, backed: &[bool], bus: &Bus, access: &Access) {
    let Some(slot) = bus.slot_at(access.address) else {
        return;
    };
    if backed[slot] {
        return;
    }
    let peripheral = bus.slots()[slot].peripheral();
    let entry = report
        .unimplemented
        .entry(access.address)
        .or_insert_with(|| {
            let offset = access.address - peripheral.base;
            Unimplemented {
                instance: peripheral.name,
                register: peripheral
                    .registers
                    .iter()
                    .find(|register| {
                        (u64::from(register.offset)..u64::from(register.end())).contains(&offset)
                    })
                    .map_or("?", |register| register.name),
                program_counter: access.program_counter,
                reads: 0,
                writes: 0,
                written: None,
            }
        });
    match access.kind {
        AccessKind::Read => entry.reads += 1,
        AccessKind::Write => {
            entry.writes += 1;
            entry.written = Some(access.value);
        }
    }
}

// An access the bus refused: an address no register covers is a gap in the model, and one a gate shut is
// the part saying no. They are counted apart because the first is work to do and the second is the model.
fn refuse(report: &mut Report, address: u64, program_counter: Option<u64>, fault: Fault) {
    match program_counter {
        Some(program_counter) if !fault.is_shut_gate() => {
            report.unmapped.entry(address).or_insert(program_counter);
        }
        // A model's access carries no program counter, so nothing of its can be read as a gap in the map.
        _ => {
            report.refused.entry(address).or_insert(fault);
        }
    }
}

impl Report {
    /// Lets every model of `mcu` look outside at `at`, settles what that moved, routes what it raised,
    /// and answers the error a settle met, if one did.
    #[expect(
        clippy::too_many_arguments,
        reason = "each parameter is its own piece of context"
    )]
    fn look_outside(
        &mut self,
        mcu: &mut dyn Mcu,
        at: VirtualNanos,
        instructions: u64,
        route: fn(u16) -> Option<InterruptLine>,
        host: &mut dyn Host,
    ) -> Option<Error> {
        mcu.poll(at);
        let error = match mcu.settle(host) {
            Ok(refusals) => {
                note(self, refusals);
                None
            }
            Err(error) => Some(error),
        };
        note_told(self, mcu.drain_told().into_iter());
        // device.rs's request_reset promises no core instruction between the ask and the reset.
        if !take_model_reset(self, mcu, at, instructions, host) {
            apply(
                route,
                mcu.drain_interrupts().into_iter(),
                host,
                &mut self.unrouted,
            );
        }
        error
    }
}

// Whether a model's reset was taken, which tells the caller to leave this settle's interrupts undelivered.
fn take_model_reset(
    report: &mut Report,
    mcu: &mut dyn Mcu,
    at: VirtualNanos,
    instructions: u64,
    host: &mut dyn Host,
) -> bool {
    let Some(cause) = mcu.take_reset_request() else {
        return false;
    };
    mcu.reset(cause);
    report.resets.push(Reset {
        at,
        instructions,
        cause,
    });
    host.request_reset();
    true
}

fn note(report: &mut Report, refusals: Vec<Refused>) {
    for refusal in refusals {
        refuse(report, refusal.address, None, refusal.fault);
    }
}

fn deadline_before(next_event: Option<VirtualNanos>, until: VirtualNanos) -> VirtualNanos {
    next_event.map_or(until, |at| at.min(until))
}

// ceiling: a route is a pure function from an interrupt to a line; upgrade when the first bus-side interrupt
// controller needs a change delivered to its model instead of a line.
fn apply(
    route: fn(u16) -> Option<InterruptLine>,
    changes: impl Iterator<Item = InterruptChange>,
    host: &mut dyn Host,
    unrouted: &mut BTreeSet<(&'static str, u16)>,
) {
    for change in changes {
        match route(change.interrupt) {
            Some(line) => host.set_interrupt(line, change.level),
            None => {
                unrouted.insert((change.instance, change.interrupt));
            }
        }
    }
}